Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
loudness_compensation.hpp
Go to the documentation of this file.
1#pragma once
2
61
63#include <pulp/signal/fft.hpp>
67
68#include <algorithm>
69#include <array>
70#include <cmath>
71#include <complex>
72#include <cstddef>
73#include <cstdint>
74#include <memory>
75#include <span>
76#include <vector>
77
78namespace pulp::signal {
79
80// Every stage below runs on an audio thread, except prepare(), which allocates
81// on the control thread. None of them reads a clock, sleeps, starts a thread
82// or takes a lock.
83
162
164 float max_cut_db = 24.0f;
165 float max_boost_db = 24.0f;
166};
167
172 std::span<const double> ww;
173 std::span<const double> dd;
174 std::span<const double> wd_re;
175 std::span<const double> wd_im;
176};
177
178// ── 1. K-weighting ──────────────────────────────────────────────────────────
179
183[[nodiscard]] inline double k_weighting_power(double hz,
185 double sample_rate) noexcept {
186 if (!k.valid || !(sample_rate > 0.0))
187 return 1.0;
188 const auto to_biquad = [](const pulp::signal::LoudnessBiquadCoefficients& c) {
189 return pulp::signal::BiquadCoefficientsT<double>{c.b0, c.b1, c.b2, c.a1, c.a2};
190 };
191 const double omega = pulp::signal::angular_frequency(hz, sample_rate);
192 const double m = pulp::signal::section_magnitude(to_biquad(k.shelf), omega) *
193 pulp::signal::section_magnitude(to_biquad(k.high_pass), omega);
194 return m * m;
195}
196
197// ── 4. The pure make-up ────────────────────────────────────────────────────
198
199[[nodiscard]] inline float clamp_makeup_db_(double ratio, MakeupLimits limits) noexcept {
200 if (!(ratio > 0.0))
201 return limits.max_boost_db;
202 return static_cast<float>(std::clamp(-10.0 * std::log10(ratio),
203 -static_cast<double>(limits.max_cut_db),
204 static_cast<double>(limits.max_boost_db)));
205}
206
211[[nodiscard]] inline float blend_makeup_gain_db(std::span<const std::complex<double>> response,
212 const LegSpectra& s, double mix,
213 MakeupLimits limits) noexcept {
214 const std::size_t n =
215 std::min({response.size(), s.ww.size(), s.dd.size(), s.wd_re.size(), s.wd_im.size()});
216 const double m = std::clamp(mix, 0.0, 1.0);
217 const double a = m * m, b = (1.0 - m) * (1.0 - m), c = 2.0 * m * (1.0 - m);
218 double out = 0.0, flat = 0.0;
219 for (std::size_t k = 0; k < n; ++k) {
220 const auto h = response[k];
221 out += a * std::norm(h) * s.ww[k] + b * s.dd[k] +
222 c * (h.real() * s.wd_re[k] - h.imag() * s.wd_im[k]);
223 flat += a * s.ww[k] + b * s.dd[k] + c * s.wd_re[k];
224 }
225 if (!(flat > 0.0))
226 return 0.0f;
227 return clamp_makeup_db_(std::max(0.0, out) / flat, limits);
228}
229
232[[nodiscard]] inline float makeup_gain_db(std::span<const double> response_power,
233 std::span<const double> spectrum_power,
234 MakeupLimits limits) noexcept {
235 const std::size_t n = std::min(response_power.size(), spectrum_power.size());
236 double total = 0.0, energy = 0.0;
237 for (std::size_t i = 0; i < n; ++i) {
238 const double w = spectrum_power[i];
239 if (!(w > 0.0))
240 continue;
241 total += w;
242 energy += w * std::max(0.0, response_power[i]);
243 }
244 if (!(total > 0.0))
245 return 0.0f;
246 return clamp_makeup_db_(energy / total, limits);
247}
248
251 double lo_hz = 0.0;
252 double hi_hz = 0.0;
253 double power_gain = 1.0;
254};
255
259template <typename WeightFn>
260[[nodiscard]] inline float band_makeup_gain_db(std::span<const BandPowerGain> bands,
261 WeightFn&& weight, MakeupLimits limits) noexcept {
262 double total = 0.0, energy = 0.0;
263 for (const auto& band : bands) {
264 const double w = weight(band.lo_hz, band.hi_hz);
265 if (!(w > 0.0))
266 continue;
267 total += w;
268 energy += w * std::max(0.0, band.power_gain);
269 }
270 if (!(total > 0.0))
271 return 0.0f;
272 return clamp_makeup_db_(energy / total, limits);
273}
274
278 public:
280 void prepare(std::size_t bins) {
281 cumulative_.assign(bins + 1, 0.0);
282 }
283
285 void assign(std::span<const double> power, double bin_hz) noexcept {
286 bin_hz_ = bin_hz > 0.0 ? bin_hz : 1.0;
287 bins_ = std::min(power.size(), cumulative_.empty() ? 0 : cumulative_.size() - 1);
288 if (bins_ == 0)
289 return;
290 cumulative_[0] = 0.0;
291 double sum = 0.0;
292 for (std::size_t k = 0; k < bins_; ++k) {
293 sum += std::max(0.0, power[k]);
294 cumulative_[k + 1] = sum;
295 }
296 if (sum > 0.0)
297 for (std::size_t k = 0; k <= bins_; ++k)
298 cumulative_[k] /= sum;
299 }
300
302 [[nodiscard]] double weight(double lo_hz, double hi_hz) const noexcept {
303 if (bins_ == 0 || !(hi_hz > lo_hz))
304 return 0.0;
305 return at_(hi_hz) - at_(lo_hz);
306 }
307
308 private:
309 [[nodiscard]] double at_(double hz) const noexcept {
310 const double x = std::max(0.0, hz / bin_hz_ + 0.5);
311 const auto i = static_cast<std::size_t>(std::floor(x));
312 if (i >= bins_)
313 return cumulative_[bins_];
314 const double t = x - static_cast<double>(i);
315 return cumulative_[i] + t * (cumulative_[i + 1] - cumulative_[i]);
316 }
317
318 std::vector<double> cumulative_;
319 std::size_t bins_ = 0;
320 double bin_hz_ = 1.0;
321};
322
323// ── 3. Minimum-phase response ───────────────────────────────────────────────
324
330 public:
332 void prepare(int fft_size) {
333 n_ = fft_size;
334 fft_ = std::make_unique<pulp::signal::FftT<float>>(n_);
335 work_.assign(static_cast<std::size_t>(n_), {});
336 }
337 [[nodiscard]] bool prepared() const noexcept {
338 return fft_ && fft_->ready();
339 }
340
342 bool compute(std::span<const double> magnitude, double floor,
343 std::span<std::complex<double>> out) noexcept {
344 const auto bins = static_cast<std::size_t>(n_ / 2 + 1);
345 if (!prepared() || magnitude.size() < bins || out.size() < bins)
346 return false;
347 const double lo = floor > 0.0 ? floor : 1.0e-12;
348 for (std::size_t k = 0; k < bins; ++k)
349 work_[k] = {static_cast<float>(std::log(std::max(lo, magnitude[k]))), 0.0f};
350 for (std::size_t k = bins; k < static_cast<std::size_t>(n_); ++k)
351 work_[k] = work_[static_cast<std::size_t>(n_) - k];
352 fft_->inverse(work_.data()); // real, even cepstrum
353 const auto half = static_cast<std::size_t>(n_ / 2);
354 for (std::size_t q = 1; q < half; ++q) {
355 work_[q] = {2.0f * work_[q].real(), 0.0f};
356 work_[static_cast<std::size_t>(n_) - q] = {0.0f, 0.0f};
357 }
358 work_[0] = {work_[0].real(), 0.0f};
359 work_[half] = {work_[half].real(), 0.0f};
360 fft_->forward(work_.data()); // ln|H| + j phase
361 for (std::size_t k = 0; k < bins; ++k)
362 out[k] = std::exp(std::complex<double>(work_[k].real(), work_[k].imag()));
363 return true;
364 }
365
366 private:
367 int n_ = 0;
368 std::unique_ptr<pulp::signal::FftT<float>> fft_;
369 std::vector<std::complex<float>> work_;
370};
371
372// ── 2. The long-term spectrum ──────────────────────────────────────────────
373
381 static constexpr int kBands = 160;
382 static constexpr double kLowHz = 10.0;
383 static constexpr double kHighHz = 24000.0;
384 bool valid = false;
385 double level_ms = 0.0;
386 std::array<float, kBands> ww{}, dd{}, re{}, im{};
387
388 [[nodiscard]] static int band_of(double hz) noexcept {
389 if (!(hz > kLowHz))
390 return 0;
391 const double x =
392 std::log(hz / kLowHz) / std::log(kHighHz / kLowHz) * static_cast<double>(kBands);
393 return std::clamp(static_cast<int>(std::floor(x)), 0, kBands - 1);
394 }
395 [[nodiscard]] static double centre_hz(int band) noexcept {
396 return kLowHz * std::pow(kHighHz / kLowHz,
397 (static_cast<double>(band) + 0.5) / static_cast<double>(kBands));
398 }
399};
400
410 public:
411 double threshold_db = 4.0;
412 int frames = 12;
413 double huge_db = 12.0;
414 int huge_frames = 6;
424 double drop_threshold_db = 0.0;
425 double drop_level_db = 3.0;
433 double level_fast_alpha = 0.3;
434 double level_slow_alpha = 0.03;
435
437
438 void reset() noexcept {
439 run_ = 0;
440 huge_run_ = 0;
441 holdoff_ = 0;
442 level_fast_ = level_slow_ = 0.0;
443 close_run_ = last_close_run_ = level_frames_ = 0;
444 clear_fit_();
445 }
446 [[nodiscard]] int run() const noexcept {
447 return run_;
448 }
449
450 [[nodiscard]] double level_drop_db() const noexcept {
451 return level_fast_ > 0.0 && level_slow_ > 0.0 ? 10.0 * std::log10(level_slow_ / level_fast_)
452 : 0.0;
453 }
454
455 [[nodiscard]] bool last_was_drop() const noexcept {
456 return last_drop_;
457 }
458
461 Event observe(float fast_db, float slow_db, double frame_lufs = kNoLevel) noexcept {
462 const bool levelled = frame_lufs > kNoLevel;
463 if (levelled) {
464 const double p = std::pow(10.0, frame_lufs / 10.0);
465 level_fast_ =
466 level_fast_ > 0.0 ? level_fast_ + level_fast_alpha * (p - level_fast_) : p;
467 level_slow_ =
468 level_slow_ > 0.0 ? level_slow_ + level_slow_alpha * (p - level_slow_) : p;
469 // The slow level means something only once it spans its own
470 // time constant (about drop_min_loud_frames).
471 if (++level_frames_ <= drop_min_loud_frames) {
472 } else if (level_drop_db() < 0.5 * drop_level_db) {
473 ++close_run_;
474 } else if (close_run_ > 0) {
475 last_close_run_ = close_run_;
476 close_run_ = 0;
477 }
478 }
479 if (holdoff_ > 0) {
480 --holdoff_;
481 return end_run_();
482 }
483 const double gap = std::abs(static_cast<double>(fast_db) - slow_db);
484 const bool dropped = levelled && drop_threshold_db > 0.0 &&
486 last_close_run_ >= drop_min_loud_frames;
487 const double threshold = dropped ? std::min(threshold_db, drop_threshold_db) : threshold_db;
488 if (!(gap > threshold)) {
489 if (run_ == 0)
490 return Event::none;
491 run_ = std::max(0, run_ - 2);
492 huge_run_ = std::max(0, huge_run_ - 2);
493 if (run_ == 0)
494 clear_fit_();
495 return run_ == 0 ? Event::run_broken : Event::none;
496 }
497 const bool started = run_ == 0;
498 if (started)
499 clear_fit_();
500 ++run_;
501 if (levelled)
502 fit_(frame_lufs);
503 huge_run_ = gap > huge_db ? huge_run_ + 1 : std::max(0, huge_run_ - 2);
504 if (run_ >= frames || huge_run_ >= huge_frames) {
505 // Below the ordinary threshold only through the quieter path,
506 // and only for steady material (not a decay).
507 const bool via_drop = !(gap > threshold_db) && huge_run_ < huge_frames;
508 if (via_drop && slope_() < -max_decay_db_per_frame) {
509 // A decay: keep the run, never confirm it on this path.
510 run_ = frames - 1;
511 return Event::none;
512 }
513 run_ = 0;
514 huge_run_ = 0;
515 clear_fit_();
516 holdoff_ = holdoff_frames;
517 last_drop_ = via_drop;
518 return Event::confirmed;
519 }
520 return started ? Event::run_started : Event::none;
521 }
522 void hold_off() noexcept {
523 end_run_();
524 holdoff_ = holdoff_frames;
525 }
526
527 static constexpr double kNoLevel = -1.0e300;
528
529 private:
530 Event end_run_() noexcept {
531 const bool had = run_ > 0;
532 run_ = 0;
533 huge_run_ = 0;
534 clear_fit_();
535 return had ? Event::run_broken : Event::none;
536 }
537 void clear_fit_() noexcept {
538 fit_n_ = 0;
539 fit_x_ = fit_y_ = fit_xx_ = fit_xy_ = 0.0;
540 }
541 void fit_(double y) noexcept {
542 const double x = static_cast<double>(fit_n_++);
543 fit_x_ += x;
544 fit_y_ += y;
545 fit_xx_ += x * x;
546 fit_xy_ += x * y;
547 }
548 // Least-squares slope of the run's frame loudness, dB per frame.
549 [[nodiscard]] double slope_() const noexcept {
550 if (fit_n_ < 3)
551 return 0.0;
552 const double n = static_cast<double>(fit_n_);
553 const double den = n * fit_xx_ - fit_x_ * fit_x_;
554 return den > 0.0 ? (n * fit_xy_ - fit_x_ * fit_y_) / den : 0.0;
555 }
556 int run_ = 0;
557 int huge_run_ = 0;
558 int holdoff_ = 0;
559 double level_fast_ = 0.0, level_slow_ = 0.0;
560 int close_run_ = 0, last_close_run_ = 0, level_frames_ = 0;
561 int fit_n_ = 0;
562 double fit_x_ = 0.0, fit_y_ = 0.0, fit_xx_ = 0.0, fit_xy_ = 0.0;
563 bool last_drop_ = false;
564};
565
567 public:
571 void prepare(double sample_rate, int channels, const LoudnessCompensationConfig& config,
572 std::span<const double> prior = {}) {
573 sample_rate_ = sample_rate > 0.0 ? sample_rate : 48000.0;
574 channels_ = std::max(1, channels);
575 config_ = config;
576 fft_size_ = fft_size_for(sample_rate_, config);
577 hop_ = fft_size_ / 2;
578 bins_ = fft_size_ / 2 + 1;
579 bin_hz_ = sample_rate_ / static_cast<double>(fft_size_);
580 const auto nb = static_cast<std::size_t>(bins_);
581 fft_ = std::make_unique<pulp::signal::FftT<float>>(fft_size_);
584 const int legs = config.track_dry_leg ? 2 : 1;
585 history_.assign(static_cast<std::size_t>(legs * channels_ * fft_size_), 0.0f);
586 frame_in_.assign(static_cast<std::size_t>(fft_size_), 0.0f);
587 wet_spectrum_.assign(static_cast<std::size_t>(fft_size_), {});
588 dry_spectrum_.assign(static_cast<std::size_t>(fft_size_), {});
589 for (auto* v : {&frame_ww_, &frame_dd_, &frame_re_, &frame_im_, &k_power_, &prior_})
590 v->assign(nb, 0.0);
591 slow_.assign(nb);
592 fast_.assign(nb);
593 fast_shape_.assign(nb, 0.0);
594 slow_shape_.assign(nb, 0.0);
595 candidate_.assign(nb);
596 previous_.assign(nb);
597 previous_shape_.assign(nb, 0.0);
598 arrived_shape_.assign(nb, 0.0);
599 alt_a_shape_.assign(nb, 0.0);
600 alt_b_shape_.assign(nb, 0.0);
601 size_published_();
602 flat_.assign(nb, 1.0);
603 zero_.assign(nb, 0.0);
604 cdf_.prepare(nb);
605 double window_energy = 0.0;
606 for (const float w : window_)
607 window_energy += static_cast<double>(w) * w;
608 // Parseval over the half spectrum (FftT::forward_real is unit-scaled):
609 // mean square of the windowed frame = 2 sum_half |X|^2 / (N sum w^2).
610 power_to_mean_square_ =
611 window_energy > 0.0 ? 2.0 / (static_cast<double>(fft_size_) * window_energy) : 0.0;
612 const auto k = pulp::signal::k_weighting_coefficients(sample_rate_);
613 for (int b = 0; b < bins_; ++b)
614 k_power_[static_cast<std::size_t>(b)] =
615 k_weighting_power(static_cast<double>(b) * bin_hz_, k, sample_rate_);
616 double total = 0.0;
617 if (prior.size() >= nb)
618 for (std::size_t b = 0; b < nb; ++b)
619 total += std::max(0.0, prior[b]);
620 for (std::size_t b = 0; b < nb; ++b)
621 prior_[b] =
622 total > 0.0 ? std::max(0.0, prior[b]) / total : 1.0 / static_cast<double>(nb);
623 const double hop_seconds = static_cast<double>(hop_) / sample_rate_;
624 alpha_ = config.time_constant_seconds > 0.0
625 ? 1.0 - std::exp(-hop_seconds / config.time_constant_seconds)
626 : 1.0;
627 fast_alpha_ = config.fast_time_constant_seconds > 0.0
628 ? 1.0 - std::exp(-hop_seconds / config.fast_time_constant_seconds)
629 : 1.0;
630 prior_step_ = config.prior_seconds > 0.0 ? hop_seconds / config.prior_seconds : 1.0;
631 local_alpha_ = config.local_level_seconds > 0.0
632 ? 1.0 - std::exp(-hop_seconds / config.local_level_seconds)
633 : 1.0;
634 alt_alpha_ = config.alternating_time_constant_seconds > 0.0
635 ? 1.0 - std::exp(-hop_seconds / config.alternating_time_constant_seconds)
636 : alpha_;
637 memory_frames_ = static_cast<std::int64_t>(std::ceil(config.memory_seconds / hop_seconds));
638 geometry_rate_ = sample_rate_;
639 geometry_channels_ = channels_;
640 level_drop_frames_ =
641 std::clamp(static_cast<int>(std::ceil(config.level_drop_window_seconds / hop_seconds)),
642 1, kMaxRecent);
643 dry_drop_frames_ = std::clamp(
644 static_cast<int>(std::ceil(config.dry_level_drop_window_seconds / hop_seconds)), 1,
645 kMaxRecent);
646 prepared_ = fft_ && fft_->ready() && static_cast<int>(window_.size()) == fft_size_;
647 reset();
648 }
649
651 [[nodiscard]] static int fft_size_for(double sample_rate,
652 const LoudnessCompensationConfig& config) noexcept {
653 if (config.fft_size > 0) {
654 int n = 64;
655 while (n < config.fft_size && n < 65536)
656 n *= 2;
657 return n;
658 }
659 int n = 1024;
660 while (static_cast<double>(n) < config.frame_seconds * sample_rate && n < 65536)
661 n *= 2;
662 return n;
663 }
664 [[nodiscard]] static int bins_for(double sample_rate,
665 const LoudnessCompensationConfig& config) noexcept {
666 return fft_size_for(sample_rate, config) / 2 + 1;
667 }
668
671 void reset() noexcept {
672 slow_.clear();
673 fast_.clear();
674 slow_frames_ = 0;
675 fast_frames_ = 0;
676 shape_frames_ = 0;
677 forget_memory_();
678 candidate_frames_ = 0;
679 candidate_active_ = false;
680 std::fill(fast_shape_.begin(), fast_shape_.end(), 0.0);
681 std::fill(slow_shape_.begin(), slow_shape_.end(), 0.0);
682 prior_weight_ = 1.0;
683 prior_fade_scale_ = 1.0;
684 level_local_ = 0.0;
685 observed_frames_ = 0;
686 gated_frames_ = 0;
687 level_drops_ = 0;
688 restarts_ = 0;
689 recent_count_ = 0;
690 recent_pos_ = 0;
691 drop_run_ = 0;
692 settle_frames_ = 0;
693 restart_grid();
694 publish_();
695 }
696
700 void restart_grid() noexcept {
701 std::fill(history_.begin(), history_.end(), 0.0f);
702 write_pos_ = 0;
703 filled_ = 0;
704 hop_pos_ = 0;
705 }
706
712 void begin_candidate() noexcept {
713 candidate_.clear();
714 candidate_frames_ = 0;
715 candidate_active_ = true;
716 candidate_skip_ = fft_size_ / std::max(1, hop_) - 1;
717 }
718
719 void cancel_candidate() noexcept {
720 candidate_active_ = false;
721 candidate_frames_ = 0;
722 }
723
731 void restart() noexcept {
732 if (slow_frames_ > 0) {
733 previous_.copy_from(slow_);
734 std::copy(slow_shape_.begin(), slow_shape_.end(), previous_shape_.begin());
735 previous_valid_ = true;
736 frames_since_previous_ = 0;
737 }
738 alternating_ = false;
739 if (candidate_active_ && candidate_frames_ > 0) {
740 slow_.copy_from(candidate_);
741 fast_.copy_from(candidate_);
742 slow_frames_ = candidate_frames_;
743 fast_frames_ = std::min(candidate_frames_, fast_equivalent_frames_());
744 } else {
745 slow_.clear();
746 fast_.clear();
747 slow_frames_ = 0;
748 fast_frames_ = 0;
749 }
751 std::copy(fast_shape_.begin(), fast_shape_.end(), slow_shape_.begin());
752 std::copy(fast_shape_.begin(), fast_shape_.end(), arrived_shape_.begin());
753 arrived_valid_ = true;
754 frames_since_arrival_ = 0;
755 rearm_shapes_();
756 recent_count_ = 0;
757 drop_run_ = 0;
758 ++restarts_;
759 if (slow_frames_ > 0)
760 publish_();
761 }
762
768 void merge_with_previous() noexcept {
769 if (!previous_valid_)
770 return;
771 std::copy(previous_shape_.begin(), previous_shape_.end(), alt_a_shape_.begin());
772 std::copy(slow_shape_.begin(), slow_shape_.end(), alt_b_shape_.begin());
773 const auto mix = [](std::vector<double>& a, const std::vector<double>& b) {
774 for (std::size_t k = 0; k < a.size(); ++k)
775 a[k] = 0.5 * (a[k] + b[k]);
776 };
777 mix(slow_.ww, previous_.ww);
778 mix(slow_.dd, previous_.dd);
779 mix(slow_.re, previous_.re);
780 mix(slow_.im, previous_.im);
781 for (std::size_t k = 0; k < slow_shape_.size(); ++k)
782 slow_shape_[k] = 0.5 * (alt_a_shape_[k] + alt_b_shape_[k]);
783 slow_frames_ = std::max<std::int64_t>(
784 slow_frames_, static_cast<std::int64_t>(std::ceil(1.0 / std::max(alpha_, 1e-9))));
785 alternating_ = true;
786 previous_valid_ = false;
788 ++merges_;
789 publish_();
790 }
791
796 void settle_alternation() noexcept {
797 std::copy(arrived_shape_.begin(), arrived_shape_.end(), alt_a_shape_.begin());
798 std::copy(slow_shape_.begin(), slow_shape_.end(), alt_b_shape_.begin());
799 alternating_ = true;
800 arrived_valid_ = false;
801 previous_valid_ = false;
803 ++merges_;
804 }
805 [[nodiscard]] bool has_arrived() const noexcept {
806 return arrived_valid_;
807 }
808 [[nodiscard]] LegSpectra arrived_shape_legs() const noexcept {
809 if (!config_.weight_by_material)
810 return {flat_, flat_, flat_, zero_};
811 return {arrived_shape_, arrived_shape_, arrived_shape_, zero_};
812 }
813 [[nodiscard]] bool has_previous() const noexcept {
814 return previous_valid_;
815 }
816 [[nodiscard]] bool alternating() const noexcept {
817 return alternating_;
818 }
819 [[nodiscard]] LegSpectra previous_shape_legs() const noexcept {
820 if (!config_.weight_by_material)
821 return {flat_, flat_, flat_, zero_};
822 return {previous_shape_, previous_shape_, previous_shape_, zero_};
823 }
824
825 [[nodiscard]] LegSpectra alternate_shape_legs(int which) const noexcept {
826 if (!config_.weight_by_material)
827 return {flat_, flat_, flat_, zero_};
828 const auto& v = which == 0 ? alt_a_shape_ : alt_b_shape_;
829 return {v, v, v, zero_};
830 }
831 [[nodiscard]] std::uint64_t merges() const noexcept {
832 return merges_;
833 }
834
835 [[nodiscard]] bool prepared_for(double sample_rate, int channels,
836 const LoudnessCompensationConfig& config) const noexcept {
837 return prepared_ && geometry_rate_ == sample_rate &&
838 geometry_channels_ == std::max(1, channels) &&
839 fft_size_ == fft_size_for(sample_rate, config);
840 }
841
842 [[nodiscard]] std::int64_t frames_since_restart() const noexcept {
843 return slow_frames_;
844 }
845
850 void wet_becomes_dry() noexcept {
851 if (!config_.track_dry_leg)
852 return;
853 std::copy(fast_.dd.begin(), fast_.dd.end(), fast_.ww.begin());
854 std::copy(fast_.dd.begin(), fast_.dd.end(), fast_.re.begin());
855 std::fill(fast_.im.begin(), fast_.im.end(), 0.0);
856 slow_.copy_from(fast_);
857 slow_frames_ = std::max<std::int64_t>(1, fast_equivalent_frames_());
858 {
859 const double t = fast_.total();
860 for (int b = 0; b < bins_; ++b) {
861 const auto i = static_cast<std::size_t>(b);
862 slow_shape_[i] = fast_shape_[i] = t > 0.0 ? fast_.ww[i] / t : 0.0;
863 }
864 }
865 rearm_shapes_();
866 forget_memory_();
868 recent_count_ = 0;
869 // The next frames' windows still hold the hold's last moments: they
870 // describe neither leg, so they wait.
871 settle_frames_ = fft_size_ / std::max(1, hop_);
872 publish_();
873 }
874
877 void legs_uncorrelated() noexcept {
878 if (!config_.track_dry_leg)
879 return;
880 for (auto* e : {&slow_, &fast_}) {
881 std::fill(e->re.begin(), e->re.end(), 0.0);
882 std::fill(e->im.begin(), e->im.end(), 0.0);
883 }
884 publish_();
885 }
886
892 template <typename OnFrame>
893 void push(const float* const* wet, const float* const* dry, int channels, int num_samples,
894 OnFrame&& on_frame) noexcept {
895 if (!prepared_ || wet == nullptr || num_samples <= 0)
896 return;
897 const int count = std::min(channels, channels_);
898 const bool two = config_.track_dry_leg;
899 int done = 0;
900 while (done < num_samples) {
901 const int chunk = std::min(num_samples - done, hop_ - hop_pos_);
902 for (int leg = 0; leg < (two ? 2 : 1); ++leg) {
903 const float* const* x = leg == 0 ? wet : (dry != nullptr ? dry : wet);
904 for (int ch = 0; ch < count; ++ch)
905 record_(ring_(leg, ch), x[ch], done, chunk);
906 for (int ch = count; ch < channels_; ++ch)
907 record_silence_(ring_(leg, ch), chunk);
908 }
909 write_pos_ = (write_pos_ + chunk) % fft_size_;
910 filled_ = std::min(fft_size_, filled_ + chunk);
911 hop_pos_ += chunk;
912 done += chunk;
913 if (hop_pos_ == hop_) {
914 hop_pos_ = 0;
915 if (filled_ == fft_size_) {
916 frame_();
917 on_frame(done);
918 }
919 }
920 }
921 }
922 template <typename OnFrame>
923 void push(const float* const* x, int channels, int num_samples, OnFrame&& on_frame) noexcept {
924 push(x, nullptr, channels, num_samples, on_frame);
925 }
926 void push(const float* const* x, int channels, int num_samples) noexcept {
927 push(x, nullptr, channels, num_samples, [](int) {});
928 }
929
930 // ── session state ──────────────────────────────────────────────────────
932 void export_bands(SpectrumBands& out) const noexcept {
933 out = SpectrumBands{};
934 if (!prepared_ || (observed_frames_ == 0 && prior_weight_ >= 1.0))
935 return;
936 const auto& e = published_;
937 double total = 0.0;
938 for (int b = 0; b < bins_; ++b) {
939 const auto i = static_cast<std::size_t>(b);
940 const int band = SpectrumBands::band_of(static_cast<double>(b) * bin_hz_);
941 out.ww[static_cast<std::size_t>(band)] += static_cast<float>(e.ww[i]);
942 out.dd[static_cast<std::size_t>(band)] += static_cast<float>(e.dd[i]);
943 out.re[static_cast<std::size_t>(band)] += static_cast<float>(e.re[i]);
944 out.im[static_cast<std::size_t>(band)] += static_cast<float>(e.im[i]);
945 total += e.ww[i];
946 }
947 if (!(total > 0.0))
948 return;
949 for (int band = 0; band < SpectrumBands::kBands; ++band) {
950 const auto j = static_cast<std::size_t>(band);
951 out.ww[j] = static_cast<float>(out.ww[j] / total);
952 out.dd[j] = static_cast<float>(out.dd[j] / total);
953 out.re[j] = static_cast<float>(out.re[j] / total);
954 out.im[j] = static_cast<float>(out.im[j] / total);
955 }
956 out.level_ms = level_local_ * power_to_mean_square_;
957 out.valid = true;
958 }
959
961 enum class Import {
969 };
970
973 void import_bands(const SpectrumBands& in, Import how = Import::warm) noexcept {
974 if (!prepared_ || !in.valid || !(in.level_ms > 0.0) || !(power_to_mean_square_ > 0.0))
975 return;
976 std::array<int, SpectrumBands::kBands> count{};
977 for (int b = 0; b < bins_; ++b)
978 ++count[static_cast<std::size_t>(
979 SpectrumBands::band_of(static_cast<double>(b) * bin_hz_))];
980 Estimate& e = candidate_; // scratch: nothing is being collected now
981 e.clear();
982 // The estimates are in units of the local level (see frame_): the
983 // saved spectrum sums to one frame's worth, and the level comes back
984 // as the local level the next frames are measured against.
985 const double total = 1.0;
986 level_local_ = in.level_ms / power_to_mean_square_;
987 for (int band = 0; band < SpectrumBands::kBands; ++band) {
988 const auto j = static_cast<std::size_t>(band);
989 if (count[j] > 0)
990 continue;
991 const auto b = static_cast<std::size_t>(
992 std::clamp(static_cast<int>(std::lround(SpectrumBands::centre_hz(band) / bin_hz_)),
993 0, bins_ - 1));
994 e.ww[b] += total * in.ww[j];
995 e.dd[b] += total * in.dd[j];
996 e.re[b] += total * in.re[j];
997 e.im[b] += total * in.im[j];
998 }
999 for (int b = 0; b < bins_; ++b) {
1000 const auto i = static_cast<std::size_t>(b);
1001 const auto j =
1002 static_cast<std::size_t>(SpectrumBands::band_of(static_cast<double>(b) * bin_hz_));
1003 if (count[j] == 0)
1004 continue;
1005 const double share = total / static_cast<double>(count[j]);
1006 e.ww[i] += share * in.ww[j];
1007 e.dd[i] += share * in.dd[j];
1008 e.re[i] += share * in.re[j];
1009 e.im[i] += share * in.im[j];
1010 }
1011 if (!config_.track_dry_leg) {
1012 e.dd = e.ww;
1013 e.re = e.ww;
1014 std::fill(e.im.begin(), e.im.end(), 0.0);
1015 }
1016 if (how == Import::prior) {
1017 const double t = e.total();
1018 for (int b = 0; b < bins_; ++b) {
1019 const auto i = static_cast<std::size_t>(b);
1020 prior_[i] = t > 0.0 ? std::max(0.0, e.ww[i]) / t : 0.0;
1021 }
1022 e.clear();
1023 reset();
1024 return;
1025 }
1026 slow_.copy_from(e);
1027 fast_.copy_from(e);
1028 e.clear();
1029 const double t = slow_.total();
1030 for (int b = 0; b < bins_; ++b) {
1031 const auto i = static_cast<std::size_t>(b);
1032 slow_shape_[i] = fast_shape_[i] = t > 0.0 ? slow_.ww[i] / t : 0.0;
1033 }
1034 prior_weight_ = 0.0;
1035 slow_frames_ = static_cast<std::int64_t>(std::ceil(1.0 / std::max(alpha_, 1e-9)));
1036 fast_frames_ = fast_equivalent_frames_();
1037 rearm_shapes_();
1038
1039 forget_memory_();
1040 candidate_frames_ = 0;
1041 candidate_active_ = false;
1042 observed_frames_ = std::max<std::uint64_t>(observed_frames_, 1);
1043 recent_count_ = 0;
1044 drop_run_ = 0;
1045 restart_grid();
1046 publish_();
1047 }
1048
1049 // ── readings ────────────────────────────────────────────────────────────
1050 [[nodiscard]] bool prepared() const noexcept {
1051 return prepared_;
1052 }
1053 [[nodiscard]] int fft_size() const noexcept {
1054 return fft_size_;
1055 }
1056 [[nodiscard]] int hop() const noexcept {
1057 return hop_;
1058 }
1059
1060 [[nodiscard]] int samples_to_next_frame() const noexcept {
1061 return hop_ - hop_pos_;
1062 }
1063 [[nodiscard]] int bins() const noexcept {
1064 return bins_;
1065 }
1066 [[nodiscard]] double bin_hz() const noexcept {
1067 return bin_hz_;
1068 }
1069 [[nodiscard]] double sample_rate() const noexcept {
1070 return sample_rate_;
1071 }
1072 [[nodiscard]] double hop_seconds() const noexcept {
1073 return static_cast<double>(hop_) / sample_rate_;
1074 }
1075 [[nodiscard]] const LoudnessCompensationConfig& config() const noexcept {
1076 return config_;
1077 }
1078 [[nodiscard]] std::uint64_t observed_frames() const noexcept {
1079 return observed_frames_;
1080 }
1081
1082 [[nodiscard]] std::uint64_t gated_frames() const noexcept {
1083 return gated_frames_;
1084 }
1085 [[nodiscard]] std::uint64_t level_drops() const noexcept {
1086 return level_drops_;
1087 }
1088 [[nodiscard]] std::uint64_t restarts() const noexcept {
1089 return restarts_;
1090 }
1091
1092 [[nodiscard]] double last_frame_lufs() const noexcept {
1093 return last_frame_lufs_;
1094 }
1095
1096 [[nodiscard]] double prior_weight() const noexcept {
1097 return prior_weight_;
1098 }
1099
1100 [[nodiscard]] LegSpectra prior_legs() const noexcept {
1101 return {prior_, prior_, prior_, zero_};
1102 }
1103
1107 void set_prior_fade_scale(double scale) noexcept {
1108 prior_fade_scale_ = std::clamp(scale, 0.0, 1.0);
1109 }
1110
1111 [[nodiscard]] bool last_frame_audible() const noexcept {
1112 return last_audible_;
1113 }
1114
1116 [[nodiscard]] std::span<const double> spectrum() const noexcept {
1117 return config_.weight_by_material ? std::span<const double>(published_.ww) : flat_;
1118 }
1119
1120 [[nodiscard]] LegSpectra legs() const noexcept {
1121 return legs_of_(published_);
1122 }
1123
1124 [[nodiscard]] LegSpectra fast_legs() const noexcept {
1125 return legs_of_(published_fast_);
1126 }
1127
1129 [[nodiscard]] LegSpectra fast_shape_legs() const noexcept {
1130 if (!config_.weight_by_material)
1131 return {flat_, flat_, flat_, zero_};
1132 return {fast_shape_, fast_shape_, fast_shape_, zero_};
1133 }
1134 [[nodiscard]] LegSpectra slow_shape_legs() const noexcept {
1135 if (!config_.weight_by_material)
1136 return {flat_, flat_, flat_, zero_};
1137 return {slow_shape_, slow_shape_, slow_shape_, zero_};
1138 }
1139 [[nodiscard]] std::int64_t shape_frames() const noexcept {
1140 return shape_frames_;
1141 }
1142
1143 [[nodiscard]] double weight(double lo_hz, double hi_hz) const noexcept {
1144 return cdf_.weight(lo_hz, hi_hz);
1145 }
1146 [[nodiscard]] const SpectrumCdf& cdf() const noexcept {
1147 return cdf_;
1148 }
1149
1150 private:
1151 static constexpr int kMaxRecent = 128;
1152
1153 // The detector's slow shape, re-armed after a restart or a release: it
1154 // is the new material's from now on, averaging on the slow constant
1155 // (not a short running mean that would track the fast one for seconds).
1156 void rearm_shapes_() noexcept {
1157 shape_frames_ = static_cast<std::int64_t>(std::ceil(1.0 / std::max(alpha_, 1e-9)));
1158 }
1159 void forget_memory_() noexcept {
1160 previous_valid_ = false;
1161 arrived_valid_ = false;
1162 alternating_ = false;
1163 frames_since_previous_ = 0;
1164 }
1165
1166 struct Estimate {
1167 std::vector<double> ww, dd, re, im;
1168 void assign(std::size_t n) {
1169 for (auto* v : {&ww, &dd, &re, &im})
1170 v->assign(n, 0.0);
1171 }
1172 void clear() noexcept {
1173 for (auto* v : {&ww, &dd, &re, &im})
1174 std::fill(v->begin(), v->end(), 0.0);
1175 }
1176 void copy_from(const Estimate& o) noexcept {
1177 std::copy(o.ww.begin(), o.ww.end(), ww.begin());
1178 std::copy(o.dd.begin(), o.dd.end(), dd.begin());
1179 std::copy(o.re.begin(), o.re.end(), re.begin());
1180 std::copy(o.im.begin(), o.im.end(), im.begin());
1181 }
1182 void scale(double k) noexcept {
1183 for (auto* v : {&ww, &dd, &re, &im})
1184 for (auto& x : *v)
1185 x *= k;
1186 }
1187 [[nodiscard]] double total() const noexcept {
1188 double t = 0.0;
1189 for (const double x : ww)
1190 t += x;
1191 return t;
1192 }
1193 };
1194
1195 [[nodiscard]] LegSpectra legs_of_(const Estimate& e) const noexcept {
1196 if (!config_.weight_by_material)
1197 return {flat_, flat_, flat_, zero_};
1198 if (!config_.track_dry_leg)
1199 return {e.ww, e.ww, e.ww, zero_};
1200 return {e.ww, e.dd, e.re, e.im};
1201 }
1202 [[nodiscard]] std::int64_t fast_equivalent_frames_() const noexcept {
1203 return static_cast<std::int64_t>(std::ceil(1.0 / std::max(fast_alpha_, 1e-9)));
1204 }
1205 [[nodiscard]] double level_total_() const noexcept {
1206 return slow_frames_ > 0 ? slow_.total() : 0.0;
1207 }
1208
1209 [[nodiscard]] float* ring_(int leg, int ch) noexcept {
1210 return history_.data() + static_cast<std::size_t>((leg * channels_ + ch) * fft_size_);
1211 }
1212 void record_(float* ring, const float* src, int offset, int count) noexcept {
1213 if (src == nullptr)
1214 return;
1215 int pos = write_pos_;
1216 for (int i = 0; i < count; ++i) {
1217 const float v = src[offset + i];
1218 ring[pos] = std::isfinite(v) ? v : 0.0f;
1219 if (++pos == fft_size_)
1220 pos = 0;
1221 }
1222 }
1223 void record_silence_(float* ring, int count) noexcept {
1224 int pos = write_pos_;
1225 for (int i = 0; i < count; ++i) {
1226 ring[pos] = 0.0f;
1227 if (++pos == fft_size_)
1228 pos = 0;
1229 }
1230 }
1231 void transform_(float* ring, std::vector<std::complex<float>>& out) noexcept {
1232 // Oldest sample first: write_pos_ is where the next one lands.
1233 for (int i = 0; i < fft_size_; ++i) {
1234 int at = write_pos_ + i;
1235 if (at >= fft_size_)
1236 at -= fft_size_;
1237 frame_in_[static_cast<std::size_t>(i)] =
1238 ring[at] * window_[static_cast<std::size_t>(i)];
1239 }
1240 fft_->forward_real(frame_in_.data(), out.data());
1241 }
1242
1243 void accumulate_(Estimate& e, double a) noexcept {
1244 const bool two = config_.track_dry_leg;
1245 for (int b = 0; b < bins_; ++b) {
1246 const auto i = static_cast<std::size_t>(b);
1247 e.ww[i] += a * (frame_ww_[i] - e.ww[i]);
1248 if (two) {
1249 e.dd[i] += a * (frame_dd_[i] - e.dd[i]);
1250 e.re[i] += a * (frame_re_[i] - e.re[i]);
1251 e.im[i] += a * (frame_im_[i] - e.im[i]);
1252 }
1253 }
1254 }
1255
1256 void frame_() noexcept {
1257 const bool two = config_.track_dry_leg;
1258 for (auto* v : {&frame_ww_, &frame_dd_, &frame_re_, &frame_im_})
1259 std::fill(v->begin(), v->end(), 0.0);
1260 for (int ch = 0; ch < channels_; ++ch) {
1261 transform_(ring_(0, ch), wet_spectrum_);
1262 if (two)
1263 transform_(ring_(1, ch), dry_spectrum_);
1264 for (int b = 0; b < bins_; ++b) {
1265 const auto i = static_cast<std::size_t>(b);
1266 const std::complex<double> w(wet_spectrum_[i].real(), wet_spectrum_[i].imag());
1267 frame_ww_[i] += std::norm(w);
1268 if (two) {
1269 const std::complex<double> d(dry_spectrum_[i].real(), dry_spectrum_[i].imag());
1270 frame_dd_[i] += std::norm(d);
1271 const auto x = w * std::conj(d);
1272 frame_re_[i] += x.real();
1273 frame_im_[i] += x.imag();
1274 }
1275 }
1276 }
1277 double total = 0.0;
1278 for (int b = 0; b < bins_; ++b) {
1279 const auto i = static_cast<std::size_t>(b);
1280 const double kp = k_power_[i];
1281 frame_ww_[i] *= kp;
1282 frame_dd_[i] *= kp;
1283 frame_re_[i] *= kp;
1284 frame_im_[i] *= kp;
1285 total += frame_ww_[i];
1286 }
1287 const double mean_square = total * power_to_mean_square_;
1288 last_frame_lufs_ = mean_square > 0.0 ? -0.691 + 10.0 * std::log10(mean_square) : -1.0e9;
1289 last_audible_ = last_frame_lufs_ >= config_.gate_lufs;
1290 if (!last_audible_) {
1291 ++gated_frames_;
1292 return;
1293 }
1294 if (settle_frames_ > 0) {
1295 --settle_frames_;
1296 return;
1297 }
1298 ++observed_frames_;
1299 // Energy weighting RELATIVE TO THE LOCAL LEVEL: each frame's power is
1300 // divided by a short (local_level_seconds) average of frame power
1301 // before it is averaged. Inside a groove that local level hardly
1302 // moves, so the kick still outweighs its tail exactly as integrated
1303 // loudness weighs it; but a swell, a fade or a quieter new part no
1304 // longer lets the loud past dominate the spectrum for seconds.
1305 level_local_ =
1306 level_local_ > 0.0 ? level_local_ + local_alpha_ * (total - level_local_) : total;
1307 // A frame's weight is (its power / the local level)^exponent: 1 is
1308 // energy weighting within the local window, 0 every frame equal.
1309 double norm = level_local_ > 0.0 ? 1.0 / level_local_ : 0.0;
1310 if (config_.level_weight_exponent != 1.0 && total > 0.0 && level_local_ > 0.0)
1311 norm = std::pow(total / level_local_, config_.level_weight_exponent) / total;
1312 for (int b = 0; b < bins_; ++b) {
1313 const auto i = static_cast<std::size_t>(b);
1314 frame_ww_[i] *= norm;
1315 frame_dd_[i] *= norm;
1316 frame_re_[i] *= norm;
1317 frame_im_[i] *= norm;
1318 }
1319 total *= norm;
1320 // Both estimates: a running mean until they hold as many frames as
1321 // their time constant spans, an exponential average after.
1322 ++slow_frames_;
1323 ++fast_frames_;
1324 const double slow_alpha = alternating_ ? alt_alpha_ : alpha_;
1325 const double a_slow =
1326 slow_alpha >= 1.0 ? 1.0 : std::max(slow_alpha, 1.0 / static_cast<double>(slow_frames_));
1327 const double a_fast = fast_alpha_ >= 1.0
1328 ? 1.0
1329 : std::max(fast_alpha_, 1.0 / static_cast<double>(fast_frames_));
1330 accumulate_(slow_, a_slow);
1331 accumulate_(fast_, a_fast);
1332 if (candidate_active_) {
1333 if (candidate_skip_ > 0) {
1334 --candidate_skip_;
1335 } else {
1336 ++candidate_frames_;
1337 accumulate_(candidate_, 1.0 / static_cast<double>(candidate_frames_));
1338 }
1339 }
1340 if (previous_valid_ && ++frames_since_previous_ > memory_frames_)
1341 previous_valid_ = false;
1342 if (arrived_valid_ && ++frames_since_arrival_ > memory_frames_)
1343 arrived_valid_ = false;
1344 // The change detector's pair: the same averages of each frame's
1345 // SHAPE (every audible frame weighs the same), so a change of
1346 // material shows within a few frames however much quieter it is.
1347 {
1348 ++shape_frames_;
1349 const double shape_alpha = alternating_ ? alt_alpha_ : alpha_;
1350 const double sa = shape_alpha >= 1.0
1351 ? 1.0
1352 : std::max(shape_alpha, 1.0 / static_cast<double>(shape_frames_));
1353 const double fa = fast_alpha_ >= 1.0
1354 ? 1.0
1355 : std::max(fast_alpha_, 1.0 / static_cast<double>(shape_frames_));
1356 const double inv = total > 0.0 ? 1.0 / total : 0.0;
1357 for (int b = 0; b < bins_; ++b) {
1358 const auto i = static_cast<std::size_t>(b);
1359 const double x = frame_ww_[i] * inv;
1360 slow_shape_[i] += sa * (x - slow_shape_[i]);
1361 fast_shape_[i] += fa * (x - fast_shape_[i]);
1362 }
1363 }
1364 double dry_total = total;
1365 if (two) {
1366 dry_total = 0.0;
1367 for (const double x : frame_dd_)
1368 dry_total += x;
1369 }
1370 level_drop_(total, dry_total);
1371 prior_weight_ = std::max(0.0, prior_weight_ - prior_step_ * prior_fade_scale_);
1372 publish_();
1373 }
1374
1375 // Energy weighting lets loud past material outweigh quieter new material
1376 // for as long as it lingers. When no frame of the last
1377 // level_drop_window_seconds has come within level_drop_db of the slow
1378 // estimate's level, the level genuinely dropped (inside a groove the
1379 // loud hits keep reaching it), so both estimates are rescaled to the
1380 // recent level and the new material weighs as much as the old.
1381 void level_drop_(double wet_total, double dry_total) noexcept {
1382 const auto slot = static_cast<std::size_t>(recent_pos_);
1383 recent_[slot] = wet_total;
1384 recent_dry_[slot] = dry_total;
1385 recent_pos_ = (recent_pos_ + 1) % kMaxRecent;
1386 recent_count_ = std::min(recent_count_ + 1, kMaxRecent);
1387 if (!(config_.level_drop_db > 0.0))
1388 return;
1389 const auto max_of = [&](const std::array<double, kMaxRecent>& ring, int frames) {
1390 double m = 0.0;
1391 for (int k = 1; k <= frames; ++k)
1392 m = std::max(
1393 m, ring[static_cast<std::size_t>((recent_pos_ - k + kMaxRecent) % kMaxRecent)]);
1394 return m;
1395 };
1396 const double floor = std::pow(10.0, -config_.level_drop_db / 10.0);
1397 double rw = 1.0, rd = 1.0, wet_max = 0.0, dry_max = 0.0;
1398 if (recent_count_ >= level_drop_frames_) {
1399 wet_max = max_of(recent_, level_drop_frames_);
1400 const double wet_level = sum_(slow_.ww);
1401 if (wet_level > 0.0 && wet_max < wet_level * floor)
1402 rw = wet_max / wet_level;
1403 }
1404 // The live (dry) leg has no change detector of its own -- it is the
1405 // dry half of a Mix, or the live input under a hold -- so its level
1406 // follows over the short window.
1407 if (config_.track_dry_leg && recent_count_ >= dry_drop_frames_) {
1408 dry_max = max_of(recent_dry_, dry_drop_frames_);
1409 const double dry_level = sum_(slow_.dd);
1410 if (dry_level > 0.0 && dry_max < dry_level * floor)
1411 rd = dry_max / dry_level;
1412 }
1413 if (!config_.track_dry_leg)
1414 rd = rw;
1415 drop_run_ = rw < 1.0 ? drop_run_ + 1 : 0;
1416 dry_drop_run_ = rd < 1.0 ? dry_drop_run_ + 1 : 0;
1417 if (drop_run_ < 2)
1418 rw = 1.0;
1419 if (dry_drop_run_ < 2)
1420 rd = 1.0;
1421 if (rw >= 1.0 && rd >= 1.0)
1422 return;
1423 // Level only: the spectrum is the change detector's business.
1424 rescale_legs_(slow_, rw, rd);
1425 const double fw = sum_(fast_.ww);
1426 const double fd = config_.track_dry_leg ? sum_(fast_.dd) : fw;
1427 rescale_legs_(fast_, rw < 1.0 && fw > wet_max && wet_max > 0.0 ? wet_max / fw : 1.0,
1428 rd < 1.0 && fd > dry_max && dry_max > 0.0 ? dry_max / fd : 1.0);
1429 if (previous_valid_)
1430 rescale_legs_(previous_, rw, rd);
1431 if (rw < 1.0)
1432 drop_run_ = 0;
1433 if (rd < 1.0)
1434 dry_drop_run_ = 0;
1435 recent_count_ = 0;
1436 ++level_drops_;
1437 }
1438
1439 // Scale the wet leg's power by rw and the dry leg's by rd; their
1440 // cross-spectrum scales by the geometric mean.
1441 void rescale_legs_(Estimate& e, double rw, double rd) noexcept {
1442 const double rx = std::sqrt(rw * rd);
1443 for (auto& x : e.ww)
1444 x *= rw;
1445 if (!config_.track_dry_leg)
1446 return;
1447 for (auto& x : e.dd)
1448 x *= rd;
1449 for (auto& x : e.re)
1450 x *= rx;
1451 for (auto& x : e.im)
1452 x *= rx;
1453 }
1454 [[nodiscard]] static double sum_(const std::vector<double>& v) noexcept {
1455 double t = 0.0;
1456 for (const double x : v)
1457 t += x;
1458 return t;
1459 }
1460
1461 // The published estimates: the prior's decaying share plus the material's
1462 // averages, each on its own wet leg's scale (its total is 1 - prior share).
1463 void blend_into_(const Estimate& avg, std::int64_t frames, Estimate& out) noexcept {
1464 // A restart cleared the averages: keep publishing the last estimate
1465 // until new material arrives (the prior is for a cold start only).
1466 if (frames == 0 && observed_frames_ > 0)
1467 return;
1468 const double total = frames > 0 ? avg.total() : 0.0;
1469 const double pw = frames > 0 && total > 0.0 ? prior_weight_ : 1.0;
1470 const double scale = total > 0.0 ? (1.0 - pw) / total : 0.0;
1471 const bool two = config_.track_dry_leg;
1472 for (int b = 0; b < bins_; ++b) {
1473 const auto i = static_cast<std::size_t>(b);
1474 const double p = pw * prior_[i];
1475 out.ww[i] = p + scale * avg.ww[i];
1476 if (two) {
1477 out.dd[i] = p + scale * avg.dd[i];
1478 out.re[i] = p + scale * avg.re[i];
1479 out.im[i] = scale * avg.im[i];
1480 }
1481 }
1482 }
1483 void publish_() noexcept {
1484 if (published_.ww.size() != static_cast<std::size_t>(bins_)) {
1485 // prepare() sizes these; first publish after it.
1486 return;
1487 }
1488 blend_into_(slow_, slow_frames_, published_);
1489 blend_into_(fast_, fast_frames_, published_fast_);
1490 cdf_.assign(spectrum(), bin_hz_);
1491 }
1492
1493 // Sizes the published estimates (prepare-time; part of prepare()).
1494 void size_published_() {
1495 published_.assign(static_cast<std::size_t>(bins_));
1496 published_fast_.assign(static_cast<std::size_t>(bins_));
1497 }
1498
1499 double sample_rate_ = 48000.0;
1500 int channels_ = 1;
1501 LoudnessCompensationConfig config_{};
1502 int fft_size_ = 4096;
1503 int hop_ = 2048;
1504 int bins_ = 2049;
1505 double bin_hz_ = 48000.0 / 4096.0;
1506 double power_to_mean_square_ = 0.0;
1507 double alpha_ = 0.0;
1508 double fast_alpha_ = 0.0;
1509 double prior_step_ = 1.0;
1510 double prior_fade_scale_ = 1.0;
1511 double prior_weight_ = 1.0;
1512 double level_local_ = 0.0;
1513 double local_alpha_ = 1.0;
1514 bool prepared_ = false;
1515 std::unique_ptr<pulp::signal::FftT<float>> fft_;
1516 std::vector<float> window_;
1517 std::vector<float> history_;
1518 std::vector<float> frame_in_;
1519 std::vector<std::complex<float>> wet_spectrum_;
1520 std::vector<std::complex<float>> dry_spectrum_;
1521 std::vector<double> frame_ww_, frame_dd_, frame_re_, frame_im_;
1522 Estimate slow_, fast_, published_, published_fast_;
1523 std::vector<double> k_power_;
1524 std::vector<double> prior_;
1525 std::vector<double> flat_;
1526 std::vector<double> zero_;
1527 SpectrumCdf cdf_;
1528 std::int64_t slow_frames_ = 0, fast_frames_ = 0, shape_frames_ = 0;
1529 Estimate candidate_, previous_;
1530 std::int64_t candidate_frames_ = 0;
1531 int candidate_skip_ = 0;
1532 bool candidate_active_ = false;
1533 std::vector<double> previous_shape_, alt_a_shape_, alt_b_shape_, arrived_shape_;
1534 bool arrived_valid_ = false;
1535 std::int64_t frames_since_arrival_ = 0;
1536 bool previous_valid_ = false;
1537 bool alternating_ = false;
1538 std::int64_t frames_since_previous_ = 0, memory_frames_ = 0;
1539 std::uint64_t merges_ = 0;
1540 double alt_alpha_ = 0.0;
1541 double geometry_rate_ = 0.0;
1542 int geometry_channels_ = 0;
1543 std::vector<double> fast_shape_, slow_shape_;
1544 std::uint64_t observed_frames_ = 0;
1545 std::uint64_t gated_frames_ = 0;
1546 std::uint64_t level_drops_ = 0;
1547 std::uint64_t restarts_ = 0;
1548 std::array<double, kMaxRecent> recent_{}, recent_dry_{};
1549 int recent_pos_ = 0, recent_count_ = 0, level_drop_frames_ = 9, drop_run_ = 0;
1550 int dry_drop_frames_ = 9, dry_drop_run_ = 0;
1551 int settle_frames_ = 0;
1552 double last_frame_lufs_ = -1.0e9;
1553 bool last_audible_ = false;
1554 int write_pos_ = 0;
1555 int filled_ = 0;
1556 int hop_pos_ = 0;
1557};
1558
1559// ── 5. The target ───────────────────────────────────────────────────────────
1560
1570 public:
1571 void reset() noexcept {
1572 primed_ = false;
1573 value_db_ = 0.0f;
1574 }
1575 [[nodiscard]] bool primed() const noexcept {
1576 return primed_;
1577 }
1578 [[nodiscard]] float value_db() const noexcept {
1579 return value_db_;
1580 }
1581
1583 bool retarget(float db) noexcept {
1584 const bool moved = !primed_ || db != value_db_;
1585 value_db_ = db;
1586 primed_ = true;
1587 return moved;
1588 }
1589
1592 bool follow(float wanted_db, double seconds, double slew_db_per_second) noexcept {
1593 return follow(wanted_db, seconds, slew_db_per_second, 0.0, 0.0);
1594 }
1595
1598 bool follow(float wanted_db, double seconds, double min_slew_db_per_second,
1599 double rate_per_second, double max_slew_db_per_second,
1600 double knee_db = 0.0) noexcept {
1601 if (!primed_)
1602 return retarget(wanted_db);
1603 float next = wanted_db;
1604 if (min_slew_db_per_second > 0.0) {
1605 const double distance = std::abs(static_cast<double>(wanted_db) - value_db_);
1606 double slew = std::max(min_slew_db_per_second,
1607 rate_per_second * std::max(0.0, distance - knee_db));
1608 if (max_slew_db_per_second > 0.0)
1609 slew = std::min(slew, max_slew_db_per_second);
1610 slew = std::max(slew, min_slew_db_per_second);
1611 const auto step = static_cast<float>(slew * std::max(0.0, seconds));
1612 next = value_db_ + std::clamp(wanted_db - value_db_, -step, step);
1613 }
1614 if (next == value_db_)
1615 return false;
1616 value_db_ = next;
1617 return true;
1618 }
1619
1620 private:
1621 bool primed_ = false;
1622 float value_db_ = 0.0f;
1623};
1624
1625} // namespace pulp::signal
Definition loudness_compensation.hpp:566
const LoudnessCompensationConfig & config() const noexcept
Definition loudness_compensation.hpp:1075
bool prepared() const noexcept
Definition loudness_compensation.hpp:1050
int samples_to_next_frame() const noexcept
Samples until the next frame boundary of the grid.
Definition loudness_compensation.hpp:1060
int hop() const noexcept
Definition loudness_compensation.hpp:1056
void legs_uncorrelated() noexcept
Freeze engaged: the wet leg is now a held past, unrelated to the live input from here on.
Definition loudness_compensation.hpp:877
void restart() noexcept
The material changed.
Definition loudness_compensation.hpp:731
void push(const float *const *x, int channels, int num_samples) noexcept
Definition loudness_compensation.hpp:926
double weight(double lo_hz, double hi_hz) const noexcept
The slow wet estimate's energy between two frequencies; the whole weighs 1.
Definition loudness_compensation.hpp:1143
void reset() noexcept
Cold start: back to the prior, and the frame grid restarts at the next sample.
Definition loudness_compensation.hpp:671
LegSpectra fast_shape_legs() const noexcept
The change detector's pair: fast and slow averages of each audible frame's wet SHAPE (level-independe...
Definition loudness_compensation.hpp:1129
void prepare(double sample_rate, int channels, const LoudnessCompensationConfig &config, std::span< const double > prior={})
Control thread; allocates.
Definition loudness_compensation.hpp:571
void export_bands(SpectrumBands &out) const noexcept
The slow estimate, band-compressed (allocation-free).
Definition loudness_compensation.hpp:932
LegSpectra arrived_shape_legs() const noexcept
Definition loudness_compensation.hpp:808
int bins() const noexcept
Definition loudness_compensation.hpp:1063
std::uint64_t observed_frames() const noexcept
Definition loudness_compensation.hpp:1078
const SpectrumCdf & cdf() const noexcept
Definition loudness_compensation.hpp:1146
bool has_arrived() const noexcept
Definition loudness_compensation.hpp:805
bool prepared_for(double sample_rate, int channels, const LoudnessCompensationConfig &config) const noexcept
The estimator has the geometry prepare() would give these arguments.
Definition loudness_compensation.hpp:835
void set_prior_fade_scale(double scale) noexcept
How fast the prior fades from the next frame on, as a multiple of prior_seconds' rate (1: as configur...
Definition loudness_compensation.hpp:1107
std::uint64_t gated_frames() const noexcept
Frames under the absolute gate (silence).
Definition loudness_compensation.hpp:1082
bool last_frame_audible() const noexcept
Whether the last frame was audible (above the absolute gate).
Definition loudness_compensation.hpp:1111
LegSpectra legs() const noexcept
All the legs' slow estimates on the wet leg's scale.
Definition loudness_compensation.hpp:1120
Import
How import_bands() treats a saved estimate.
Definition loudness_compensation.hpp:961
@ warm
As the estimate itself (the same stream, carried across a change of geometry).
Definition loudness_compensation.hpp:968
@ prior
As a better prior: playback starts at the saved estimate's level and the material's own estimate take...
Definition loudness_compensation.hpp:965
LegSpectra alternate_shape_legs(int which) const noexcept
The two remembered halves of an alternation (0: the earlier, 1: the later).
Definition loudness_compensation.hpp:825
void wet_becomes_dry() noexcept
Freeze released: the wet leg becomes the live input again, whose spectrum the dry leg kept warm throu...
Definition loudness_compensation.hpp:850
double last_frame_lufs() const noexcept
K-weighted loudness of the last frame's wet leg (LUFS-equivalent).
Definition loudness_compensation.hpp:1092
LegSpectra slow_shape_legs() const noexcept
Definition loudness_compensation.hpp:1134
double bin_hz() const noexcept
Definition loudness_compensation.hpp:1066
void settle_alternation() noexcept
The material came back to what the LAST restart went to, after something else in between that never c...
Definition loudness_compensation.hpp:796
std::int64_t shape_frames() const noexcept
Definition loudness_compensation.hpp:1139
double sample_rate() const noexcept
Definition loudness_compensation.hpp:1069
void cancel_candidate() noexcept
The run broke: that was a passing moment, not new material.
Definition loudness_compensation.hpp:719
LegSpectra prior_legs() const noexcept
The prior's per-bin share (sums to one), as legs.
Definition loudness_compensation.hpp:1100
std::uint64_t level_drops() const noexcept
Definition loudness_compensation.hpp:1085
void import_bands(const SpectrumBands &in, Import how=Import::warm) noexcept
A band's energy is spread evenly over the bins whose centres fall in it (or given to the bin nearest ...
Definition loudness_compensation.hpp:973
std::int64_t frames_since_restart() const noexcept
Audible frames averaged since the last restart (or cold start).
Definition loudness_compensation.hpp:842
std::span< const double > spectrum() const noexcept
The slow (wet) estimate per bin, K-weighted, normalised to sum 1.
Definition loudness_compensation.hpp:1116
LegSpectra previous_shape_legs() const noexcept
Definition loudness_compensation.hpp:819
static int bins_for(double sample_rate, const LoudnessCompensationConfig &config) noexcept
Definition loudness_compensation.hpp:664
bool has_previous() const noexcept
Definition loudness_compensation.hpp:813
double prior_weight() const noexcept
The prior's remaining share of the estimate (1 until material is heard).
Definition loudness_compensation.hpp:1096
void restart_grid() noexcept
A transport jump or host reset: the frame grid restarts at the next sample (so how the stream is chop...
Definition loudness_compensation.hpp:700
static int fft_size_for(double sample_rate, const LoudnessCompensationConfig &config) noexcept
The FFT size prepare() picks for a rate and config.
Definition loudness_compensation.hpp:651
void begin_candidate() noexcept
A detector run began: collect the would-be new material's own energy average, so a confirmed change s...
Definition loudness_compensation.hpp:712
std::uint64_t merges() const noexcept
Definition loudness_compensation.hpp:831
std::uint64_t restarts() const noexcept
Definition loudness_compensation.hpp:1088
bool alternating() const noexcept
Definition loudness_compensation.hpp:816
LegSpectra fast_legs() const noexcept
The same of the fast estimate.
Definition loudness_compensation.hpp:1124
void merge_with_previous() noexcept
The material went back to what it was before the last change: it alternates (a kick-only bar and a fu...
Definition loudness_compensation.hpp:768
void push(const float *const *wet, const float *const *dry, int channels, int num_samples, OnFrame &&on_frame) noexcept
Feed planar samples of the wet leg and, when tracking it, the dry leg (nullptr: the dry leg is the we...
Definition loudness_compensation.hpp:893
double hop_seconds() const noexcept
Definition loudness_compensation.hpp:1072
void push(const float *const *x, int channels, int num_samples, OnFrame &&on_frame) noexcept
Definition loudness_compensation.hpp:923
int fft_size() const noexcept
Definition loudness_compensation.hpp:1053
What the make-up gain is heading for.
Definition loudness_compensation.hpp:1569
void reset() noexcept
Definition loudness_compensation.hpp:1571
float value_db() const noexcept
Definition loudness_compensation.hpp:1578
bool follow(float wanted_db, double seconds, double slew_db_per_second) noexcept
Move toward wanted_db by at most slew x seconds (no limit when the slew is <= 0).
Definition loudness_compensation.hpp:1592
bool primed() const noexcept
Definition loudness_compensation.hpp:1575
bool follow(float wanted_db, double seconds, double min_slew_db_per_second, double rate_per_second, double max_slew_db_per_second, double knee_db=0.0) noexcept
The proportional form described above; the speed-up counts only the distance beyond knee_db.
Definition loudness_compensation.hpp:1598
bool retarget(float db) noexcept
Jump to db. Returns whether the target changed.
Definition loudness_compensation.hpp:1583
Detects a genuine change of material from the gap between the make-up the fast and the slow estimates...
Definition loudness_compensation.hpp:409
double drop_threshold_db
Quieter after louder.
Definition loudness_compensation.hpp:424
int frames
Definition loudness_compensation.hpp:412
void hold_off() noexcept
Definition loudness_compensation.hpp:522
Event
Definition loudness_compensation.hpp:436
@ run_started
Definition loudness_compensation.hpp:436
@ none
Definition loudness_compensation.hpp:436
@ run_broken
Definition loudness_compensation.hpp:436
@ confirmed
Definition loudness_compensation.hpp:436
double level_slow_alpha
Definition loudness_compensation.hpp:434
Event observe(float fast_db, float slow_db, double frame_lufs=kNoLevel) noexcept
One audible frame: the make-up the fast and slow shapes give, and (optional) the frame's loudness,...
Definition loudness_compensation.hpp:461
int run() const noexcept
Definition loudness_compensation.hpp:446
int drop_min_loud_frames
... and only after louder material that lasted: the level must have stayed within half of drop_level_...
Definition loudness_compensation.hpp:431
int huge_frames
Definition loudness_compensation.hpp:414
double threshold_db
Definition loudness_compensation.hpp:411
double drop_level_db
Definition loudness_compensation.hpp:425
double level_fast_alpha
Per-frame coefficients of the fast and slow level averages (power).
Definition loudness_compensation.hpp:433
static constexpr double kNoLevel
Definition loudness_compensation.hpp:527
void reset() noexcept
Definition loudness_compensation.hpp:438
double level_drop_db() const noexcept
The fast level's fall below the slow one, dB (>= 0 while quieter).
Definition loudness_compensation.hpp:450
double huge_db
Definition loudness_compensation.hpp:413
int holdoff_frames
Definition loudness_compensation.hpp:415
double max_decay_db_per_frame
Definition loudness_compensation.hpp:426
bool last_was_drop() const noexcept
Whether the last confirmation came through the quieter-after-louder path.
Definition loudness_compensation.hpp:455
The minimum-phase response with a given magnitude on an FFT grid, by the folded real cepstrum: ln|H| ...
Definition loudness_compensation.hpp:329
bool compute(std::span< const double > magnitude, double floor, std::span< std::complex< double > > out) noexcept
magnitude: N/2 + 1 linear magnitudes. out: N/2 + 1 values.
Definition loudness_compensation.hpp:342
void prepare(int fft_size)
Control thread; allocates. fft_size: the grid (bins = N/2 + 1).
Definition loudness_compensation.hpp:332
bool prepared() const noexcept
Definition loudness_compensation.hpp:337
The normalised cumulative of a power spectrum on a uniform bin grid.
Definition loudness_compensation.hpp:277
double weight(double lo_hz, double hi_hz) const noexcept
Energy between two frequencies; the whole spectrum weighs 1.
Definition loudness_compensation.hpp:302
void assign(std::span< const double > power, double bin_hz) noexcept
Allocation-free once prepared for at least power.size() bins.
Definition loudness_compensation.hpp:285
void prepare(std::size_t bins)
Control thread: size for bins.
Definition loudness_compensation.hpp:280
static std::vector< SampleType > generate(int size, Type type, SampleType param=SampleType{0.0f})
Definition windowing.hpp:56
@ hann
Definition windowing.hpp:45
Evaluate what a biquad (or a cascade of them) does to the spectrum.
Multi-channel metering: sample peak, RMS, K-weighted momentary and gated integrated loudness,...
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
MidiEvent at(MidiEvent event, std::int32_t sample_offset) noexcept
Definition utility_contract.hpp:140
@ d
Definition pitch.hpp:22
@ b
Definition pitch.hpp:31
@ ring
Definition string.hpp:18
Definition additive_bank.hpp:218
float band_makeup_gain_db(std::span< const BandPowerGain > bands, WeightFn &&weight, MakeupLimits limits) noexcept
The single-leg form over bands, each weighted by weight(lo_hz, hi_hz) – the material's energy between...
Definition loudness_compensation.hpp:260
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
@ c
Definition spectrum_trace.hpp:35
@ energy
sum h^2 = 1 per output ear (default; convolution reverb).
Definition zero_latency_convolver_support.hpp:160
float clamp_makeup_db_(double ratio, MakeupLimits limits) noexcept
Definition loudness_compensation.hpp:199
float makeup_gain_db(std::span< const double > response_power, std::span< const double > spectrum_power, MakeupLimits limits) noexcept
The single-leg, real-response form: per-bin power response |H|^2 against a per-bin power spectrum on ...
Definition loudness_compensation.hpp:232
float blend_makeup_gain_db(std::span< const std::complex< double > > response, const LegSpectra &s, double mix, MakeupLimits limits) noexcept
Make-up, dB, for the two-leg model in the file comment: response is the wet leg's complex response pe...
Definition loudness_compensation.hpp:211
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ a
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ o
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
double angular_frequency(double freq_hz, double sample_rate)
Angular frequency (radians/sample) of freq_hz at sample_rate, clamped to [0, π].
Definition frequency_response.hpp:97
double k_weighting_power(double hz, const pulp::signal::KWeightingCoefficients &k, double sample_rate) noexcept
|K(f)|^2 for BS.1770 K-weighting at sample_rate, through Pulp's own K-weighting design and biquad res...
Definition loudness_compensation.hpp:183
KWeightingCoefficients k_weighting_coefficients(double sample_rate)
Return the two-stage K-weighting design for a sample rate.
Definition multi_channel_meter.hpp:91
void scale(const float *a, float scalar, float *dst, std::size_t count) noexcept
@ m
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ t
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
One band of a band-wise response: its frequency span and power gain.
Definition loudness_compensation.hpp:250
double power_gain
Definition loudness_compensation.hpp:253
double hi_hz
Definition loudness_compensation.hpp:252
double lo_hz
Definition loudness_compensation.hpp:251
Definition biquad.hpp:24
Definition multi_channel_meter.hpp:81
The long-term spectra of the two legs on one bin grid, K-weighted, on a common scale.
Definition loudness_compensation.hpp:171
std::span< const double > ww
Definition loudness_compensation.hpp:172
std::span< const double > wd_re
Definition loudness_compensation.hpp:174
std::span< const double > dd
Definition loudness_compensation.hpp:173
std::span< const double > wd_im
Definition loudness_compensation.hpp:175
Definition multi_channel_meter.hpp:77
Every tunable of the stateful stages.
Definition loudness_compensation.hpp:85
double fast_time_constant_seconds
Time constant of the FAST estimate, seconds: the change detector's, and what the slow estimate restar...
Definition loudness_compensation.hpp:102
double gate_lufs
Frames below this K-weighted loudness (LUFS-equivalent over the frame, channels summed as BS....
Definition loudness_compensation.hpp:99
double memory_seconds
How long the material before the last change is remembered.
Definition loudness_compensation.hpp:122
double material_slew_max_db_per_second
Definition loudness_compensation.hpp:128
float max_boost_db
Definition loudness_compensation.hpp:149
double level_weight_exponent
Definition loudness_compensation.hpp:117
double material_slew_knee_db
The proportional speed-up counts only the distance beyond this, so a groove's wobble around the targe...
Definition loudness_compensation.hpp:131
double change_seconds
Definition loudness_compensation.hpp:136
double dry_level_drop_window_seconds
The same for the live (dry) leg, which has no change detector.
Definition loudness_compensation.hpp:111
double level_drop_db
Level-drop rule: when no audible frame in the last level_drop_window_seconds came within level_drop_d...
Definition loudness_compensation.hpp:108
double change_max_decay_db_per_second
Definition loudness_compensation.hpp:145
double change_drop_threshold_db
Quieter after louder (MaterialChangeDetector::drop_threshold_db): a gap above change_drop_threshold_d...
Definition loudness_compensation.hpp:143
bool track_dry_leg
true: push() takes a dry leg too, and the estimator keeps Pdd and Pwd.
Definition loudness_compensation.hpp:154
double local_level_seconds
Frames are weighted by their power relative to a running average of frame power over this long (see L...
Definition loudness_compensation.hpp:116
double change_drop_min_loud_seconds
Definition loudness_compensation.hpp:146
int fft_size
Definition loudness_compensation.hpp:160
double material_slew_rate_per_second
Definition loudness_compensation.hpp:127
double level_drop_window_seconds
Definition loudness_compensation.hpp:109
float max_cut_db
Make-up range.
Definition loudness_compensation.hpp:148
double change_threshold_db
Change detector (MaterialChangeDetector): the fast and slow estimates' make-up for the current shape ...
Definition loudness_compensation.hpp:135
double change_drop_level_db
Definition loudness_compensation.hpp:144
double material_slew_db_per_second
Slowest rate the material moves the make-up target, dB/s (<= 0: no limit) – and the proportional spee...
Definition loudness_compensation.hpp:126
double alternating_time_constant_seconds
Time constant of the slow estimate while the material alternates between two remembered states (see L...
Definition loudness_compensation.hpp:120
double change_huge_db
Definition loudness_compensation.hpp:137
double prior_seconds
How long the prior keeps a say once material is heard: its weight falls linearly from 1 to 0 over thi...
Definition loudness_compensation.hpp:95
bool weight_by_material
false: every bin weighs the same (ignores the material and K).
Definition loudness_compensation.hpp:152
double change_huge_seconds
Definition loudness_compensation.hpp:138
double time_constant_seconds
Time constant of the material spectrum, seconds.
Definition loudness_compensation.hpp:88
double frame_seconds
Analysis frame length, seconds; the FFT size is the next power of two at or above it (4096 at 44....
Definition loudness_compensation.hpp:157
Definition loudness_compensation.hpp:163
float max_boost_db
Definition loudness_compensation.hpp:165
float max_cut_db
Definition loudness_compensation.hpp:164
A band-compressed, rate-independent copy of a LongTermSpectrum's estimate, for saving with a session ...
Definition loudness_compensation.hpp:380
std::array< float, kBands > im
Definition loudness_compensation.hpp:386
std::array< float, kBands > re
Definition loudness_compensation.hpp:386
static double centre_hz(int band) noexcept
Definition loudness_compensation.hpp:395
static int band_of(double hz) noexcept
Definition loudness_compensation.hpp:388
std::array< float, kBands > ww
Definition loudness_compensation.hpp:386
double level_ms
Definition loudness_compensation.hpp:385
static constexpr double kHighHz
Definition loudness_compensation.hpp:383
bool valid
Definition loudness_compensation.hpp:384
static constexpr int kBands
Definition loudness_compensation.hpp:381
std::array< float, kBands > dd
Definition loudness_compensation.hpp:386
static constexpr double kLowHz
Definition loudness_compensation.hpp:382