Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
fir_design.hpp
Go to the documentation of this file.
1#pragma once
2
5
7#include <pulp/signal/fft.hpp>
8
9#include <algorithm>
10#include <cmath>
11#include <complex>
12#include <cstddef>
13#include <cstdint>
14#include <limits>
15#include <new>
16#include <numeric>
17#include <span>
18#include <stdexcept>
19#include <vector>
20
21namespace pulp::signal {
22
23inline constexpr std::uint64_t kDefaultFirDesignWorkspaceBytes = 256u * 1024u * 1024u;
24inline constexpr std::size_t kMaximumFirDesignTapCount = 1023u;
25inline constexpr std::size_t kMaximumFirDesignPointCount = 65536u;
28inline constexpr std::size_t kMaximumMinimumPhaseFirSize = 65536u;
29
43
50
53 double omega = 0.0;
56 double amplitude = 0.0;
58 double weight = 1.0;
59};
60
68
71 std::vector<double> coefficients;
72 std::vector<double> measured_amplitudes;
73 std::vector<double> errors;
74 double weighted_rms_error = 0.0;
76 std::size_t numerical_rank = 0;
80
81 explicit operator bool() const noexcept {
83 }
84};
85
92 double lower_omega = 0.0;
93 double upper_omega = 0.0;
95 double amplitude = 0.0;
98 double weight = 1.0;
99};
100
118
121 std::vector<double> coefficients;
124 std::vector<double> extremal_frequencies;
126 std::vector<double> band_ripples;
128 double minimax_error = 0.0;
132 std::size_t iterations = 0;
133
134 explicit operator bool() const noexcept {
136 }
137};
138
147
150 std::size_t fft_size = 0;
151 std::vector<double> coefficients;
152 std::vector<double> measured_magnitudes;
153 std::vector<double> errors;
154 double rms_error = 0.0;
156
157 explicit operator bool() const noexcept {
159 }
160};
161
163
164inline constexpr double pi = 3.141592653589793238462643383279502884;
165
166inline bool is_forced_zero_endpoint(LinearPhaseFirType type, double omega) noexcept {
167 const bool at_dc = omega == 0.0;
168 const bool at_nyquist = omega == pi;
169 return (type == LinearPhaseFirType::type_ii_symmetric_even && at_nyquist) ||
171 (at_dc || at_nyquist)) ||
173}
174
175} // namespace fir_design_detail
176
177inline std::complex<double> fir_response(std::span<const double> coefficients,
178 double omega) noexcept {
179 std::complex<double> response{};
180 const std::complex<double> step = std::polar(1.0, -omega);
181 std::complex<double> phase{1.0, 0.0};
182 for (double coefficient : coefficients) {
183 response += coefficient * phase;
184 phase *= step;
185 }
186 return response;
187}
188
192inline double linear_phase_fir_amplitude(std::span<const double> coefficients,
193 LinearPhaseFirType type, double omega) noexcept {
194 if (coefficients.empty())
195 return 0.0;
197 return 0.0;
198 const double delay = 0.5 * static_cast<double>(coefficients.size() - 1u);
199 const auto zero_phase = fir_response(coefficients, omega) * std::polar(1.0, delay * omega);
200 const bool antisymmetric = type == LinearPhaseFirType::type_iii_antisymmetric_odd ||
202 return antisymmetric ? zero_phase.imag() : zero_phase.real();
203}
204
205namespace fir_design_detail {
206
207inline bool valid_type_length(LinearPhaseFirType type, std::size_t count) noexcept {
208 if (count == 0u)
209 return false;
210 switch (type) {
212 return (count & 1u) != 0u;
214 return (count & 1u) == 0u;
216 return count >= 3u && (count & 1u) != 0u;
218 return (count & 1u) == 0u;
219 }
220 return false;
221}
222
224 std::size_t count) noexcept {
225 return type == LinearPhaseFirType::type_i_symmetric_odd ? count / 2u + 1u : count / 2u;
226}
227
228inline double basis_value(LinearPhaseFirType type, std::size_t column, double omega) noexcept {
229 if (is_forced_zero_endpoint(type, omega))
230 return 0.0;
231 switch (type) {
233 return column == 0u ? 1.0 : 2.0 * std::cos(static_cast<double>(column) * omega);
235 return 2.0 * std::cos((static_cast<double>(column) + 0.5) * omega);
237 return 2.0 * std::sin((static_cast<double>(column) + 1.0) * omega);
239 return 2.0 * std::sin((static_cast<double>(column) + 0.5) * omega);
240 }
241 return 0.0;
242}
243
244inline void clear_payload(FirLeastSquaresResult& result) noexcept {
245 result.coefficients.clear();
246 result.measured_amplitudes.clear();
247 result.errors.clear();
248 result.weighted_rms_error = 0.0;
249 result.maximum_absolute_error = 0.0;
250}
251
252inline void clear_payload(MinimumPhaseFirResult& result) noexcept {
253 result.fft_size = 0u;
254 result.coefficients.clear();
255 result.measured_magnitudes.clear();
256 result.errors.clear();
257 result.rms_error = 0.0;
258 result.maximum_absolute_error = 0.0;
259}
260
261inline bool is_power_of_two(std::size_t value) noexcept {
262 return value >= 2u && (value & (value - 1u)) == 0u;
263}
264
265inline bool admit_minimum_phase_workspace(std::size_t fft_size, std::size_t bins,
266 std::size_t coefficients, std::uint64_t limit) noexcept {
267 if (limit == 0u)
268 return false;
269 CheckedRetainedByteCharge charge(limit);
270 // One complex work spectrum, FftT's retained twiddles, coefficients,
271 // measured bins, and errors. No hidden full-spectrum copy is retained.
272 if (!charge.add<std::complex<double>>(fft_size) ||
273 !charge.add<std::complex<double>>(fft_size / 2u) || !charge.add<double>(coefficients) ||
274 !charge.add<double>(bins) || !charge.add<double>(bins))
275 return false;
276 return charge.total() <= limit;
277}
278
279inline bool admit_least_squares_workspace(std::size_t rows, std::size_t columns, std::size_t taps,
280 std::uint64_t limit) noexcept {
281 std::uint64_t matrix_elements = 0;
282 if (limit == 0u ||
284 rows, columns, static_cast<std::uint64_t>(std::numeric_limits<std::size_t>::max()),
285 matrix_elements))
286 return false;
287 CheckedRetainedByteCharge charge(limit);
288 if (!charge.add<double>(matrix_elements) || !charge.add<double>(rows) ||
289 !charge.add<std::size_t>(columns) || !charge.add<double>(columns) ||
290 !charge.add<double>(columns) || !charge.add<double>(columns) ||
291 !charge.add<double>(columns) || !charge.add<double>(taps) || !charge.add<double>(rows) ||
292 !charge.add<double>(rows))
293 return false;
294 return charge.total() <= limit;
295}
296
297inline void reconstruct_coefficients(LinearPhaseFirType type, std::span<const double> independent,
298 std::span<double> coefficients) noexcept {
299 const std::size_t count = coefficients.size();
300 const std::size_t half = count / 2u;
302 coefficients[half] = independent[0];
303 for (std::size_t k = 1u; k < independent.size(); ++k)
304 coefficients[half - k] = coefficients[half + k] = independent[k];
305 return;
306 }
307 const bool antisymmetric = type == LinearPhaseFirType::type_iii_antisymmetric_odd ||
310 coefficients[half] = 0.0;
311 for (std::size_t k = 0u; k < independent.size(); ++k) {
312 const std::size_t offset =
314 const std::size_t left = half - 1u - k;
315 const std::size_t right =
316 type == LinearPhaseFirType::type_iii_antisymmetric_odd ? half + offset : half + k;
317 coefficients[left] = independent[k];
318 coefficients[right] = antisymmetric ? -independent[k] : independent[k];
319 }
320}
321
322} // namespace fir_design_detail
323
332inline FirLeastSquaresResult design_fir_least_squares(std::span<const FirDesignPoint> points,
333 const FirLeastSquaresOptions& options) {
334 using namespace fir_design_detail;
336 if (options.tap_count > kMaximumFirDesignTapCount ||
337 points.size() > kMaximumFirDesignPointCount) {
339 return result;
340 }
341 if (!valid_type_length(options.type, options.tap_count) || points.empty() ||
342 !(options.rank_tolerance > 0.0) || options.rank_tolerance > 1.0 ||
343 !std::isfinite(options.rank_tolerance) ||
344 !(options.maximum_diagonal_condition_estimate >= 1.0) ||
345 !std::isfinite(options.maximum_diagonal_condition_estimate))
346 return result;
347
348 const std::size_t rows = points.size();
349 const std::size_t columns = independent_coefficient_count(options.type, options.tap_count);
350 double maximum_weight = 0.0;
351 for (const auto& point : points) {
352 if (!std::isfinite(point.omega) || point.omega < 0.0 || point.omega > pi ||
353 !std::isfinite(point.amplitude) || !(point.weight > 0.0) ||
354 !std::isfinite(point.weight))
355 return result;
356 maximum_weight = std::max(maximum_weight, point.weight);
357 }
358 if (rows < columns) {
360 return result;
361 }
362 const double maximum_weight_sqrt = std::sqrt(maximum_weight);
363 if (!admit_least_squares_workspace(rows, columns, options.tap_count,
364 options.maximum_workspace_bytes)) {
366 return result;
367 }
368
369#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
370 try {
371#endif
372 std::vector<double> matrix(rows * columns);
373 std::vector<double> rhs(rows);
374 std::vector<std::size_t> permutation(columns);
375 std::vector<double> solution(columns);
376 for (std::size_t row = 0u; row < rows; ++row) {
377 // A common positive scale does not change the minimizer. Dividing
378 // the square roots separately preserves weight ratios that remain
379 // representable even when the ratio itself would underflow.
380 const double scale = std::sqrt(points[row].weight) / maximum_weight_sqrt;
381 rhs[row] = scale * points[row].amplitude;
382 if (!std::isfinite(rhs[row])) {
384 return result;
385 }
386 for (std::size_t column = 0u; column < columns; ++column) {
387 matrix[row * columns + column] =
388 scale * basis_value(options.type, column, points[row].omega);
389 if (!std::isfinite(matrix[row * columns + column])) {
391 return result;
392 }
393 }
394 }
395 std::iota(permutation.begin(), permutation.end(), std::size_t{0});
396
397 double leading_norm = 0.0;
398 for (std::size_t column = 0u; column < columns; ++column) {
399 double norm = 0.0;
400 for (std::size_t row = 0u; row < rows; ++row)
401 norm = std::hypot(norm, matrix[row * columns + column]);
402 leading_norm = std::max(leading_norm, norm);
403 }
404 if (!std::isfinite(leading_norm)) {
406 return result;
407 }
408 if (!(leading_norm > 0.0)) {
410 return result;
411 }
412
413 const double rank_threshold = options.rank_tolerance * leading_norm;
414 double minimum_diagonal = std::numeric_limits<double>::infinity();
415 double maximum_diagonal = 0.0;
416 for (std::size_t step = 0u; step < columns; ++step) {
417 std::size_t pivot = step;
418 double pivot_norm = -1.0;
419 for (std::size_t column = step; column < columns; ++column) {
420 double norm = 0.0;
421 for (std::size_t row = step; row < rows; ++row)
422 norm = std::hypot(norm, matrix[row * columns + column]);
423 if (norm > pivot_norm) { // Strict comparison preserves the first tie.
424 pivot_norm = norm;
425 pivot = column;
426 }
427 }
428 if (!std::isfinite(pivot_norm)) {
430 result.numerical_rank = step;
431 return result;
432 }
433 if (!(pivot_norm > rank_threshold)) {
435 result.numerical_rank = step;
436 return result;
437 }
438 if (pivot != step) {
439 for (std::size_t row = 0u; row < rows; ++row)
440 std::swap(matrix[row * columns + step], matrix[row * columns + pivot]);
441 std::swap(permutation[step], permutation[pivot]);
442 }
443
444 const double x0 = matrix[step * columns + step];
445 const double alpha = x0 >= 0.0 ? -pivot_norm : pivot_norm;
446 matrix[step * columns + step] = x0 - alpha;
447 double vector_norm_squared = 0.0;
448 for (std::size_t row = step; row < rows; ++row) {
449 const double value = matrix[row * columns + step];
450 vector_norm_squared += value * value;
451 }
452 if (!(vector_norm_squared > 0.0) || !std::isfinite(vector_norm_squared)) {
454 clear_payload(result);
455 return result;
456 }
457 const double beta = 2.0 / vector_norm_squared;
458 for (std::size_t column = step + 1u; column < columns; ++column) {
459 double dot = 0.0;
460 for (std::size_t row = step; row < rows; ++row)
461 dot += matrix[row * columns + step] * matrix[row * columns + column];
462 const double factor = beta * dot;
463 for (std::size_t row = step; row < rows; ++row)
464 matrix[row * columns + column] -= factor * matrix[row * columns + step];
465 }
466 double rhs_dot = 0.0;
467 for (std::size_t row = step; row < rows; ++row)
468 rhs_dot += matrix[row * columns + step] * rhs[row];
469 const double rhs_factor = beta * rhs_dot;
470 for (std::size_t row = step; row < rows; ++row)
471 rhs[row] -= rhs_factor * matrix[row * columns + step];
472 matrix[step * columns + step] = alpha;
473 for (std::size_t row = step + 1u; row < rows; ++row)
474 matrix[row * columns + step] = 0.0;
475
476 const double diagonal = std::abs(alpha);
477 minimum_diagonal = std::min(minimum_diagonal, diagonal);
478 maximum_diagonal = std::max(maximum_diagonal, diagonal);
479 result.numerical_rank = step + 1u;
480 }
481
482 result.qr_diagonal_condition_estimate = maximum_diagonal / minimum_diagonal;
483 if (!std::isfinite(result.qr_diagonal_condition_estimate)) {
485 return result;
486 }
489 return result;
490 }
491
492 for (std::size_t reverse = columns; reverse-- > 0u;) {
493 double value = rhs[reverse];
494 for (std::size_t column = reverse + 1u; column < columns; ++column)
495 value -= matrix[reverse * columns + column] * solution[column];
496 solution[reverse] = value / matrix[reverse * columns + reverse];
497 if (!std::isfinite(solution[reverse])) {
499 clear_payload(result);
500 return result;
501 }
502 }
503
504 // Semi-normal refinement recovers small projections that can be lost
505 // when a large target component is nearly orthogonal to the design
506 // space. Limit it to systems where squaring the diagonal spread still
507 // leaves ample precision; the primary QR result handles the rest.
508 const double refinement_condition_limit =
509 1.0 / std::sqrt(std::sqrt(std::numeric_limits<double>::epsilon()));
510 if (result.qr_diagonal_condition_estimate <= refinement_condition_limit) {
511 std::vector<double> correction(columns, 0.0);
512 std::vector<double> gradient(columns, 0.0);
513 for (std::size_t row = 0u; row < rows; ++row) {
514 const double scale = std::sqrt(points[row].weight) / maximum_weight_sqrt;
515 double predicted = 0.0;
516 for (std::size_t column = 0u; column < columns; ++column) {
517 const double value =
518 scale * basis_value(options.type, permutation[column], points[row].omega);
519 predicted = std::fma(value, solution[column], predicted);
520 }
521 const double residual = scale * points[row].amplitude - predicted;
522 if (!std::isfinite(residual)) {
524 clear_payload(result);
525 return result;
526 }
527 for (std::size_t column = 0u; column < columns; ++column) {
528 const double value =
529 scale * basis_value(options.type, permutation[column], points[row].omega);
530 gradient[column] = std::fma(value, residual, gradient[column]);
531 }
532 }
533 for (std::size_t column = 0u; column < columns; ++column) {
534 double value = gradient[column];
535 if (!std::isfinite(value)) {
537 clear_payload(result);
538 return result;
539 }
540 for (std::size_t row = 0u; row < column; ++row)
541 value -= matrix[row * columns + column] * correction[row];
542 correction[column] = value / matrix[column * columns + column];
543 if (!std::isfinite(correction[column])) {
545 clear_payload(result);
546 return result;
547 }
548 }
549 for (std::size_t reverse = columns; reverse-- > 0u;) {
550 double value = correction[reverse];
551 for (std::size_t column = reverse + 1u; column < columns; ++column)
552 value -= matrix[reverse * columns + column] * correction[column];
553 correction[reverse] = value / matrix[reverse * columns + reverse];
554 if (!std::isfinite(correction[reverse])) {
556 clear_payload(result);
557 return result;
558 }
559 }
560 double linear_gain = 0.0;
561 double correction_energy = 0.0;
562 for (std::size_t row = 0u; row < columns; ++row) {
563 linear_gain = std::fma(correction[row], gradient[row], linear_gain);
564 double transformed = 0.0;
565 for (std::size_t column = row; column < columns; ++column)
566 transformed =
567 std::fma(matrix[row * columns + column], correction[column], transformed);
568 correction_energy = std::fma(transformed, transformed, correction_energy);
569 }
570 const double predicted_reduction = 2.0 * linear_gain - correction_energy;
571 if (std::isfinite(predicted_reduction) && predicted_reduction > 0.0) {
572 bool finite_candidate = true;
573 for (std::size_t column = 0u; column < columns; ++column) {
574 gradient[column] = solution[column] + correction[column];
575 finite_candidate = finite_candidate && std::isfinite(gradient[column]);
576 }
577 if (finite_candidate)
578 solution = gradient;
579 }
580 }
581 std::vector<double> unpermuted(columns);
582 for (std::size_t column = 0u; column < columns; ++column)
583 unpermuted[permutation[column]] = solution[column];
584
585 result.coefficients.assign(options.tap_count, 0.0);
586 reconstruct_coefficients(options.type, unpermuted, result.coefficients);
587 result.measured_amplitudes.resize(rows);
588 result.errors.resize(rows);
589 double weighted_squared_error = 0.0;
590 double weight_sum = 0.0;
591 for (std::size_t row = 0u; row < rows; ++row) {
592 const double measured =
593 linear_phase_fir_amplitude(result.coefficients, options.type, points[row].omega);
594 const double error = measured - points[row].amplitude;
595 if (!std::isfinite(measured) || !std::isfinite(error)) {
597 clear_payload(result);
598 return result;
599 }
600 result.measured_amplitudes[row] = measured;
601 result.errors[row] = error;
603 std::max(result.maximum_absolute_error, std::abs(error));
604 const double scale = std::sqrt(points[row].weight) / maximum_weight_sqrt;
605 const double scaled_error = scale * error;
606 if (!std::isfinite(scaled_error)) {
608 clear_payload(result);
609 return result;
610 }
611 weighted_squared_error += scaled_error * scaled_error;
612 weight_sum += scale * scale;
613 }
614 result.weighted_rms_error = std::sqrt(weighted_squared_error / weight_sum);
615 if (!std::isfinite(result.weighted_rms_error)) {
617 clear_payload(result);
618 return result;
619 }
621 return result;
622#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
623 } catch (const std::bad_alloc&) {
624 clear_payload(result);
626 return result;
627 } catch (const std::length_error&) {
628 clear_payload(result);
630 return result;
631 }
632#endif
633}
634
635namespace fir_design_detail {
636
640inline bool solve_dense_in_place(std::vector<double>& matrix, std::vector<double>& rhs,
641 std::size_t n) noexcept {
642 for (std::size_t column = 0u; column < n; ++column) {
643 std::size_t pivot = column;
644 double best = std::abs(matrix[column * n + column]);
645 for (std::size_t row = column + 1u; row < n; ++row) {
646 const double candidate = std::abs(matrix[row * n + column]);
647 if (candidate > best) {
648 best = candidate;
649 pivot = row;
650 }
651 }
652 if (!(best > 0.0) || !std::isfinite(best))
653 return false;
654 if (pivot != column) {
655 for (std::size_t k = column; k < n; ++k)
656 std::swap(matrix[pivot * n + k], matrix[column * n + k]);
657 std::swap(rhs[pivot], rhs[column]);
658 }
659 const double diagonal = matrix[column * n + column];
660 for (std::size_t row = column + 1u; row < n; ++row) {
661 const double factor = matrix[row * n + column] / diagonal;
662 if (!std::isfinite(factor))
663 return false;
664 if (factor == 0.0)
665 continue;
666 for (std::size_t k = column; k < n; ++k)
667 matrix[row * n + k] = std::fma(-factor, matrix[column * n + k], matrix[row * n + k]);
668 rhs[row] = std::fma(-factor, rhs[column], rhs[row]);
669 }
670 }
671 for (std::size_t reverse = n; reverse-- > 0u;) {
672 double value = rhs[reverse];
673 for (std::size_t k = reverse + 1u; k < n; ++k)
674 value = std::fma(-matrix[reverse * n + k], rhs[k], value);
675 const double solved = value / matrix[reverse * n + reverse];
676 if (!std::isfinite(solved))
677 return false;
678 rhs[reverse] = solved;
679 }
680 return true;
681}
682
683inline void clear_payload(FirEquirippleResult& result) noexcept {
684 result.coefficients.clear();
685 result.extremal_frequencies.clear();
686 result.band_ripples.clear();
687 result.minimax_error = 0.0;
688 result.measured_weighted_error = 0.0;
689}
690
691} // namespace fir_design_detail
692
714inline FirEquirippleResult design_fir_equiripple(std::span<const FirEquirippleBand> bands,
715 const FirEquirippleOptions& options) {
716 using namespace fir_design_detail;
717 FirEquirippleResult result;
719
720 if (bands.empty())
721 return result;
722 if (!valid_type_length(options.type, options.tap_count))
723 return result;
724 if (options.grid_density == 0u || options.maximum_iterations == 0u)
725 return result;
726 if (!(options.convergence_tolerance > 0.0) || !std::isfinite(options.convergence_tolerance))
727 return result;
728 if (!std::isfinite(options.maximum_minimax_error) || options.maximum_minimax_error < 0.0)
729 return result;
730 if (options.tap_count > kMaximumFirDesignTapCount) {
732 return result;
733 }
734
735 double previous_upper = -1.0;
736 for (const auto& band : bands) {
737 if (!std::isfinite(band.lower_omega) || !std::isfinite(band.upper_omega) ||
738 !std::isfinite(band.amplitude) || !std::isfinite(band.weight))
739 return result;
740 if (!(band.weight > 0.0))
741 return result;
742 if (band.lower_omega < 0.0 || band.upper_omega > pi)
743 return result;
744 if (!(band.lower_omega < band.upper_omega))
745 return result;
746 if (band.lower_omega <= previous_upper)
747 return result; // bands must be ascending and disjoint
748 previous_upper = band.upper_omega;
749 }
750
751 const std::size_t coefficients = independent_coefficient_count(options.type, options.tap_count);
752 if (coefficients == 0u)
753 return result;
754 const std::size_t unknowns = coefficients + 1u; // basis coefficients plus delta
755
757
758#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
759 try {
760#endif
761 // Dense grid, apportioned across bands by width so every band is
762 // represented even when one is far narrower than another.
763 double total_width = 0.0;
764 for (const auto& band : bands)
765 total_width += band.upper_omega - band.lower_omega;
766 if (!(total_width > 0.0))
767 return result;
768
769 const std::size_t requested_points = options.grid_density * unknowns;
770 if (requested_points > kMaximumFirDesignPointCount) {
772 return result;
773 }
774 const auto grid_elements = static_cast<std::uint64_t>(requested_points) + bands.size();
775 const auto square = static_cast<std::uint64_t>(unknowns) * unknowns;
776 if (!charge.add<double>(grid_elements) || !charge.add<double>(grid_elements) ||
777 !charge.add<double>(grid_elements) || !charge.add<std::size_t>(grid_elements) ||
778 !charge.add<double>(square) || !charge.add<double>(unknowns) ||
779 !charge.add<double>(coefficients) || !charge.add<double>(grid_elements)) {
781 return result;
782 }
783
784 std::vector<double> grid_omega;
785 std::vector<double> grid_desired;
786 std::vector<double> grid_weight;
787 std::vector<std::size_t> grid_band;
788 grid_omega.reserve(requested_points + bands.size());
789 for (std::size_t b = 0u; b < bands.size(); ++b) {
790 const auto& band = bands[b];
791 const double width = band.upper_omega - band.lower_omega;
792 std::size_t count = static_cast<std::size_t>(
793 std::llround(static_cast<double>(requested_points) * (width / total_width)));
794 count = std::max<std::size_t>(count, 2u);
795 for (std::size_t i = 0u; i < count; ++i) {
796 const double t = static_cast<double>(i) / static_cast<double>(count - 1u);
797 const double omega = band.lower_omega + t * width;
798 // A forced-zero endpoint cannot approximate a nonzero target,
799 // and contributes an unbounded error that would capture every
800 // exchange. Excluding it is the standard treatment.
801 if (is_forced_zero_endpoint(options.type, omega))
802 continue;
803 grid_omega.push_back(omega);
804 grid_desired.push_back(band.amplitude);
805 grid_weight.push_back(band.weight);
806 grid_band.push_back(b);
807 }
808 }
809 const std::size_t grid_size = grid_omega.size();
810 if (grid_size < unknowns)
811 return result;
812
813 // Initial alternation set: evenly spaced across the grid.
814 std::vector<std::size_t> extremal(unknowns);
815 for (std::size_t i = 0u; i < unknowns; ++i)
816 extremal[i] = static_cast<std::size_t>(
817 std::llround(static_cast<double>(i) * static_cast<double>(grid_size - 1u) /
818 static_cast<double>(unknowns - 1u)));
819
820 std::vector<double> matrix(unknowns * unknowns);
821 std::vector<double> rhs(unknowns);
822 std::vector<double> solution(coefficients);
823 std::vector<double> error(grid_size);
824 double delta = 0.0;
825 bool converged = false;
826 std::size_t iteration = 0u;
827
828 for (; iteration < options.maximum_iterations; ++iteration) {
829 for (std::size_t i = 0u; i < unknowns; ++i) {
830 const double omega = grid_omega[extremal[i]];
831 for (std::size_t k = 0u; k < coefficients; ++k)
832 matrix[i * unknowns + k] = basis_value(options.type, k, omega);
833 const double sign = (i % 2u == 0u) ? 1.0 : -1.0;
834 matrix[i * unknowns + coefficients] = -sign / grid_weight[extremal[i]];
835 rhs[i] = grid_desired[extremal[i]];
836 }
837 if (!solve_dense_in_place(matrix, rhs, unknowns)) {
839 clear_payload(result);
840 result.iterations = iteration;
841 return result;
842 }
843 for (std::size_t k = 0u; k < coefficients; ++k)
844 solution[k] = rhs[k];
845 delta = rhs[coefficients];
846 if (!std::isfinite(delta)) {
848 clear_payload(result);
849 result.iterations = iteration;
850 return result;
851 }
852
853 double peak = 0.0;
854 for (std::size_t g = 0u; g < grid_size; ++g) {
855 double amplitude = 0.0;
856 for (std::size_t k = 0u; k < coefficients; ++k)
857 amplitude = std::fma(solution[k],
858 basis_value(options.type, k, grid_omega[g]), amplitude);
859 const double weighted = grid_weight[g] * (amplitude - grid_desired[g]);
860 if (!std::isfinite(weighted)) {
862 clear_payload(result);
863 return result;
864 }
865 error[g] = weighted;
866 peak = std::max(peak, std::abs(weighted));
867 }
868
869 const double reference = std::max(std::abs(delta), 1.0e-300);
870 if (peak - std::abs(delta) <= options.convergence_tolerance * reference) {
871 converged = true;
872 ++iteration;
873 break;
874 }
875
876 // Exchange: every local maximum of |error|, then thin to a single
877 // alternating set of exactly `unknowns` points.
878 std::vector<std::size_t> candidates;
879 candidates.reserve(grid_size);
880 for (std::size_t g = 0u; g < grid_size; ++g) {
881 // A band edge is a constrained extremum and is admitted
882 // unconditionally. The approximation region stops there while
883 // the error is still free to rise, so an edge is frequently not
884 // a local maximum yet still carries an alternation. Requiring
885 // it to beat its interior neighbour drops it exactly when the
886 // error climbs into the band, which starves the alternation set
887 // and fails a design that is in fact converging.
888 const bool starts_band = g == 0u || grid_band[g] != grid_band[g - 1u];
889 const bool ends_band = g + 1u == grid_size || grid_band[g] != grid_band[g + 1u];
890 const bool interior_peak =
891 (g == 0u || std::abs(error[g]) >= std::abs(error[g - 1u])) &&
892 (g + 1u == grid_size || std::abs(error[g]) >= std::abs(error[g + 1u]));
893 if ((starts_band || ends_band || interior_peak) && std::abs(error[g]) > 0.0)
894 candidates.push_back(g);
895 }
896 // Collapse runs of the same error sign, keeping the largest.
897 std::vector<std::size_t> alternating;
898 alternating.reserve(candidates.size());
899 for (std::size_t index : candidates) {
900 if (!alternating.empty()) {
901 const double previous = error[alternating.back()];
902 if ((previous > 0.0) == (error[index] > 0.0)) {
903 if (std::abs(error[index]) > std::abs(previous))
904 alternating.back() = index;
905 continue;
906 }
907 }
908 alternating.push_back(index);
909 }
910 if (alternating.size() < unknowns) {
912 clear_payload(result);
913 result.iterations = iteration;
914 return result;
915 }
916 // Drop the weakest end repeatedly so the retained set stays
917 // contiguous in alternation and keeps the largest deviations.
918 while (alternating.size() > unknowns) {
919 if (std::abs(error[alternating.front()]) < std::abs(error[alternating.back()]))
920 alternating.erase(alternating.begin());
921 else
922 alternating.pop_back();
923 }
924 extremal = alternating;
925 }
926
927 if (!converged) {
929 clear_payload(result);
930 result.iterations = iteration;
931 return result;
932 }
933
934 result.coefficients.assign(options.tap_count, 0.0);
935 reconstruct_coefficients(options.type, solution, result.coefficients);
936 result.minimax_error = std::abs(delta);
937 result.iterations = iteration;
938
939 // Measure the delivered filter rather than trusting the solve.
940 result.band_ripples.assign(bands.size(), 0.0);
941 double measured_peak = 0.0;
942 for (std::size_t g = 0u; g < grid_size; ++g) {
943 const double amplitude =
944 linear_phase_fir_amplitude(result.coefficients, options.type, grid_omega[g]);
945 if (!std::isfinite(amplitude)) {
947 clear_payload(result);
948 return result;
949 }
950 const double deviation = std::abs(amplitude - grid_desired[g]);
951 result.band_ripples[grid_band[g]] =
952 std::max(result.band_ripples[grid_band[g]], deviation);
953 measured_peak = std::max(measured_peak, grid_weight[g] * deviation);
954 }
955 result.measured_weighted_error = measured_peak;
956
957 result.extremal_frequencies.reserve(unknowns);
958 for (std::size_t index : extremal)
959 result.extremal_frequencies.push_back(grid_omega[index]);
960 std::sort(result.extremal_frequencies.begin(), result.extremal_frequencies.end());
961
962 if (options.maximum_minimax_error > 0.0 &&
963 result.minimax_error > options.maximum_minimax_error) {
964 // The exchange succeeded; the spec did not. Report infeasibility at
965 // this tap count instead of a filter that quietly misses it.
966 clear_payload(result);
968 result.iterations = iteration;
969 return result;
970 }
971
973 return result;
974#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
975 } catch (const std::bad_alloc&) {
976 clear_payload(result);
978 return result;
979 } catch (const std::length_error&) {
980 clear_payload(result);
982 return result;
983 }
984#endif
985}
986
996inline MinimumPhaseFirResult
997reconstruct_minimum_phase_fir(std::span<const double> one_sided_magnitudes,
998 const MinimumPhaseFirOptions& options = {}) {
999 using namespace fir_design_detail;
1000 MinimumPhaseFirResult result;
1001 if (one_sided_magnitudes.size() < 2u ||
1002 one_sided_magnitudes.size() - 1u > std::numeric_limits<std::size_t>::max() / 2u ||
1003 !(options.log_magnitude_floor > 0.0) || !std::isfinite(options.log_magnitude_floor))
1004 return result;
1005 const std::size_t fft_size = 2u * (one_sided_magnitudes.size() - 1u);
1006 const std::size_t coefficient_count =
1007 options.coefficient_count == 0u ? fft_size : options.coefficient_count;
1008 if (fft_size > kMaximumMinimumPhaseFirSize ||
1009 fft_size > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
1010 result.status = FirDesignStatus::unsupported_size;
1011 return result;
1012 }
1013 if (!is_power_of_two(fft_size) || coefficient_count == 0u || coefficient_count > fft_size)
1014 return result;
1015 for (double magnitude : one_sided_magnitudes) {
1016 if (!(magnitude >= 0.0) || !std::isfinite(magnitude))
1017 return result;
1018 }
1019 if (!admit_minimum_phase_workspace(fft_size, one_sided_magnitudes.size(), coefficient_count,
1020 options.maximum_workspace_bytes)) {
1022 return result;
1023 }
1024
1025#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
1026 try {
1027#endif
1028 std::vector<std::complex<double>> spectrum(fft_size);
1029 for (std::size_t bin = 0u; bin < one_sided_magnitudes.size(); ++bin) {
1030 const double log_magnitude =
1031 std::log(std::max(one_sided_magnitudes[bin], options.log_magnitude_floor));
1032 if (!std::isfinite(log_magnitude)) {
1033 result.status = FirDesignStatus::numerical_failure;
1034 clear_payload(result);
1035 return result;
1036 }
1037 spectrum[bin] = {log_magnitude, 0.0};
1038 }
1039 for (std::size_t bin = one_sided_magnitudes.size(); bin < fft_size; ++bin)
1040 spectrum[bin] = spectrum[fft_size - bin];
1041
1042 FftT<double> fft(static_cast<int>(fft_size));
1043 fft.inverse(spectrum.data());
1044 for (const auto value : spectrum) {
1045 if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) {
1046 result.status = FirDesignStatus::numerical_failure;
1047 clear_payload(result);
1048 return result;
1049 }
1050 }
1051 for (std::size_t quefrency = 1u; quefrency < fft_size / 2u; ++quefrency)
1052 spectrum[quefrency] *= 2.0;
1053 for (std::size_t quefrency = fft_size / 2u + 1u; quefrency < fft_size; ++quefrency)
1054 spectrum[quefrency] = {};
1055 fft.forward(spectrum.data());
1056 for (auto& value : spectrum) {
1057 value = std::exp(value);
1058 if (!std::isfinite(value.real()) || !std::isfinite(value.imag())) {
1059 result.status = FirDesignStatus::numerical_failure;
1060 clear_payload(result);
1061 return result;
1062 }
1063 }
1064 fft.inverse(spectrum.data());
1065
1066 result.fft_size = fft_size;
1067 result.coefficients.resize(coefficient_count);
1068 for (std::size_t index = 0u; index < coefficient_count; ++index) {
1069 const double value = spectrum[index].real();
1070 if (!std::isfinite(value)) {
1071 result.status = FirDesignStatus::numerical_failure;
1072 clear_payload(result);
1073 return result;
1074 }
1075 result.coefficients[index] = value;
1076 }
1077
1078 std::fill(spectrum.begin(), spectrum.end(), std::complex<double>{});
1079 for (std::size_t index = 0u; index < coefficient_count; ++index)
1080 spectrum[index] = {result.coefficients[index], 0.0};
1081 fft.forward(spectrum.data());
1082 result.measured_magnitudes.resize(one_sided_magnitudes.size());
1083 result.errors.resize(one_sided_magnitudes.size());
1084 double squared_error = 0.0;
1085 for (std::size_t bin = 0u; bin < one_sided_magnitudes.size(); ++bin) {
1086 const double measured = std::abs(spectrum[bin]);
1087 const double target = std::max(one_sided_magnitudes[bin], options.log_magnitude_floor);
1088 const double error = measured - target;
1089 if (!std::isfinite(measured) || !std::isfinite(error)) {
1090 result.status = FirDesignStatus::numerical_failure;
1091 clear_payload(result);
1092 return result;
1093 }
1094 result.measured_magnitudes[bin] = measured;
1095 result.errors[bin] = error;
1096 result.maximum_absolute_error =
1097 std::max(result.maximum_absolute_error, std::abs(error));
1098 squared_error += error * error;
1099 }
1100 result.rms_error =
1101 std::sqrt(squared_error / static_cast<double>(one_sided_magnitudes.size()));
1102 if (!std::isfinite(result.rms_error)) {
1103 result.status = FirDesignStatus::numerical_failure;
1104 clear_payload(result);
1105 return result;
1106 }
1107 result.status = FirDesignStatus::success;
1108 return result;
1109#if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)
1110 } catch (const std::bad_alloc&) {
1111 clear_payload(result);
1113 return result;
1114 } catch (const std::length_error&) {
1115 clear_payload(result);
1117 return result;
1118 }
1119#endif
1120}
1121
1122} // namespace pulp::signal
Definition checked_allocation.hpp:57
bool add(std::uint64_t elements) noexcept
Definition checked_allocation.hpp:63
std::uint64_t total() const noexcept
Definition checked_allocation.hpp:83
Definition fft.hpp:176
@ error
Definition validation_harness.hpp:20
@ index
Definition gpu_pass.hpp:140
Definition fir_design.hpp:162
constexpr double pi
Definition fir_design.hpp:164
void reconstruct_coefficients(LinearPhaseFirType type, std::span< const double > independent, std::span< double > coefficients) noexcept
Definition fir_design.hpp:297
std::size_t independent_coefficient_count(LinearPhaseFirType type, std::size_t count) noexcept
Definition fir_design.hpp:223
bool valid_type_length(LinearPhaseFirType type, std::size_t count) noexcept
Definition fir_design.hpp:207
double basis_value(LinearPhaseFirType type, std::size_t column, double omega) noexcept
Definition fir_design.hpp:228
void clear_payload(FirLeastSquaresResult &result) noexcept
Definition fir_design.hpp:244
bool is_forced_zero_endpoint(LinearPhaseFirType type, double omega) noexcept
Definition fir_design.hpp:166
bool is_power_of_two(std::size_t value) noexcept
Definition fir_design.hpp:261
bool admit_minimum_phase_workspace(std::size_t fft_size, std::size_t bins, std::size_t coefficients, std::uint64_t limit) noexcept
Definition fir_design.hpp:265
bool admit_least_squares_workspace(std::size_t rows, std::size_t columns, std::size_t taps, std::uint64_t limit) noexcept
Definition fir_design.hpp:279
bool solve_dense_in_place(std::vector< double > &matrix, std::vector< double > &rhs, std::size_t n) noexcept
Solve a small dense square system in place by Gaussian elimination with partial pivoting.
Definition fir_design.hpp:640
Definition additive_bank.hpp:218
constexpr std::uint64_t kDefaultFirDesignWorkspaceBytes
Definition fir_design.hpp:23
constexpr std::size_t kMaximumFirDesignTapCount
Definition fir_design.hpp:24
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
FirLeastSquaresResult design_fir_least_squares(std::span< const FirDesignPoint > points, const FirLeastSquaresOptions &options)
Weighted least-squares design of a real Type I, II, III, or IV linear-phase FIR.
Definition fir_design.hpp:332
FirEquirippleResult design_fir_equiripple(std::span< const FirEquirippleBand > bands, const FirEquirippleOptions &options)
Equiripple (weighted minimax) design of a real Type I-IV linear-phase FIR by the Remez exchange,...
Definition fir_design.hpp:714
LinearPhaseFirType
Definition fir_design.hpp:44
@ type_iv_antisymmetric_even
Definition fir_design.hpp:48
@ type_i_symmetric_odd
Definition fir_design.hpp:45
@ type_ii_symmetric_even
Definition fir_design.hpp:46
@ type_iii_antisymmetric_odd
Definition fir_design.hpp:47
constexpr std::size_t kMaximumFirDesignPointCount
Definition fir_design.hpp:25
@ delay
Definition envelope.hpp:57
@ independent
X and Y advance only on their own clocks and never on each other's.
Definition cartesian_walk.hpp:20
@ invalid_argument
Definition comb_filter.hpp:35
std::complex< double > fir_response(std::span< const double > coefficients, double omega) noexcept
Definition fir_design.hpp:177
bool checked_capacity_product(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &product) noexcept
Definition checked_allocation.hpp:12
@ reference
Platform sine; the compatibility default.
Definition fast_math.hpp:40
double linear_phase_fir_amplitude(std::span< const double > coefficients, LinearPhaseFirType type, double omega) noexcept
Signed amplitude after removing the exact linear delay of a Type I-IV FIR.
Definition fir_design.hpp:192
FirDesignStatus
Definition fir_design.hpp:30
@ not_converged
The exchange hit its iteration cap without meeting the convergence tolerance, or the achieved minimax...
Definition fir_design.hpp:41
@ success
Definition fir_design.hpp:31
@ unsupported_size
Definition fir_design.hpp:33
@ ill_conditioned
Definition fir_design.hpp:37
@ numerical_failure
Definition fir_design.hpp:38
@ workspace_limit_exceeded
Definition fir_design.hpp:34
@ rank_deficient
Definition fir_design.hpp:36
@ allocation_failure
Definition fir_design.hpp:35
@ invalid_argument
Definition fir_design.hpp:32
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
MinimumPhaseFirResult reconstruct_minimum_phase_fir(std::span< const double > one_sided_magnitudes, const MinimumPhaseFirOptions &options={})
Reconstruct a causal minimum-phase FIR from N/2+1 nonnegative magnitude bins.
Definition fir_design.hpp:997
@ square
Definition lfo_sampling.hpp:58
@ reverse
Rise to a peak, then cut.
Definition nonlin_ambience_design.hpp:246
constexpr std::size_t kMaximumMinimumPhaseFirSize
Largest admitted radix-2 FFT size for minimum-phase reconstruction.
Definition fir_design.hpp:28
@ spectrum
Definition design_ir.hpp:265
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
Definition fir_design.hpp:51
double amplitude
Signed zero-phase amplitude.
Definition fir_design.hpp:56
double omega
Angular frequency in radians/sample, in the closed interval [0, pi].
Definition fir_design.hpp:53
double weight
Positive least-squares weight. The objective is sum(weight * error^2).
Definition fir_design.hpp:58
One contiguous approximation band for an equiripple design.
Definition fir_design.hpp:91
double amplitude
Desired signed zero-phase amplitude across the band.
Definition fir_design.hpp:95
double weight
Positive weight.
Definition fir_design.hpp:98
double lower_omega
Definition fir_design.hpp:92
double upper_omega
Definition fir_design.hpp:93
Definition fir_design.hpp:101
LinearPhaseFirType type
Definition fir_design.hpp:103
std::size_t grid_density
Dense-grid points per independent coefficient.
Definition fir_design.hpp:106
std::uint64_t maximum_workspace_bytes
Definition fir_design.hpp:116
std::size_t tap_count
Definition fir_design.hpp:102
double convergence_tolerance
Relative gap between the grid's peak error and the interpolated ripple below which the exchange is co...
Definition fir_design.hpp:111
std::size_t maximum_iterations
Bounded exchange budget. Reaching it is reported, never ignored.
Definition fir_design.hpp:108
double maximum_minimax_error
Optional requirement.
Definition fir_design.hpp:115
Definition fir_design.hpp:119
FirDesignStatus status
Definition fir_design.hpp:120
std::size_t iterations
Definition fir_design.hpp:132
std::vector< double > extremal_frequencies
The final alternation set, ascending.
Definition fir_design.hpp:124
std::vector< double > coefficients
Definition fir_design.hpp:121
std::vector< double > band_ripples
Achieved max |A(w) - amplitude| per input band, unweighted.
Definition fir_design.hpp:126
double minimax_error
The equalized weighted ripple, |delta|.
Definition fir_design.hpp:128
double measured_weighted_error
Largest weighted error measured on the dense grid at the returned design.
Definition fir_design.hpp:131
Definition fir_design.hpp:61
LinearPhaseFirType type
Definition fir_design.hpp:63
double maximum_diagonal_condition_estimate
Definition fir_design.hpp:65
std::uint64_t maximum_workspace_bytes
Definition fir_design.hpp:66
std::size_t tap_count
Definition fir_design.hpp:62
double rank_tolerance
Definition fir_design.hpp:64
Definition fir_design.hpp:69
double qr_diagonal_condition_estimate
Ratio of largest to smallest accepted diagonal of pivoted R.
Definition fir_design.hpp:79
double maximum_absolute_error
Definition fir_design.hpp:75
std::vector< double > coefficients
Definition fir_design.hpp:71
double weighted_rms_error
Definition fir_design.hpp:74
std::vector< double > measured_amplitudes
Definition fir_design.hpp:72
std::vector< double > errors
Definition fir_design.hpp:73
FirDesignStatus status
Definition fir_design.hpp:70
std::size_t numerical_rank
Definition fir_design.hpp:76
Definition fir_design.hpp:139
double log_magnitude_floor
Every magnitude below this positive floor, including zero, is replaced before log().
Definition fir_design.hpp:144
std::uint64_t maximum_workspace_bytes
Definition fir_design.hpp:145
std::size_t coefficient_count
Zero means retain the complete circular impulse of fft_size samples.
Definition fir_design.hpp:141
Definition fir_design.hpp:148
std::size_t fft_size
Definition fir_design.hpp:150
double maximum_absolute_error
Definition fir_design.hpp:155
std::vector< double > coefficients
Definition fir_design.hpp:151
FirDesignStatus status
Definition fir_design.hpp:149
std::vector< double > errors
Definition fir_design.hpp:153
std::vector< double > measured_magnitudes
Definition fir_design.hpp:152
double rms_error
Definition fir_design.hpp:154