Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
multi_channel_meter.hpp
Go to the documentation of this file.
1#pragma once
2
14
15#include <pulp/signal/detail/simd_lanes.hpp>
16
17#include <array>
18#include <cmath>
19#include <algorithm>
20#include <cstdint>
21#include <limits>
22#include <numbers>
23#include <numeric>
24#include <type_traits>
25#include <utility>
26#include <vector>
27
28namespace pulp::signal {
29
30namespace detail {
31struct MultiChannelMeterTestAccess;
32}
33
35static constexpr int kMaxMeterChannels = 16;
36
41static constexpr double kMaxMeterInputMagnitude = 1.0e6;
42
47enum class MeterFeatures : std::uint8_t {
48 levels = 0,
49 correlation = 1u << 0,
50 loudness = 1u << 1,
52};
53
55 return static_cast<MeterFeatures>(static_cast<std::uint8_t>(a) | static_cast<std::uint8_t>(b));
56}
57
58constexpr bool has_feature(MeterFeatures set, MeterFeatures feature) noexcept {
59 return (static_cast<std::uint8_t>(set) & static_cast<std::uint8_t>(feature)) != 0;
60}
61
76
78 double b0 = 1.0, b1 = 0.0, b2 = 0.0, a1 = 0.0, a2 = 0.0;
79};
80
86
92 if (!std::isfinite(sample_rate) || sample_rate <= 2.0 * 1681.974450955533)
93 return {};
94
95 constexpr double shelf_f0 = 1681.974450955533;
96 constexpr double shelf_gain_db = 3.999843853973347;
97 constexpr double shelf_q = 0.7071752369554196;
98 const double shelf_k = std::tan(std::numbers::pi * shelf_f0 / sample_rate);
99 const double vh = std::pow(10.0, shelf_gain_db / 20.0);
100 const double vb = std::pow(vh, 0.4996667741545416);
101 const double shelf_a0 = 1.0 + shelf_k / shelf_q + shelf_k * shelf_k;
102
103 constexpr double high_pass_f0 = 38.13547087602444;
104 constexpr double high_pass_q = 0.5003270373238773;
105 const double high_pass_k = std::tan(std::numbers::pi * high_pass_f0 / sample_rate);
106 const double high_pass_a0 = 1.0 + high_pass_k / high_pass_q
107 + high_pass_k * high_pass_k;
108
109 return {
110 {(vh + vb * shelf_k / shelf_q + shelf_k * shelf_k) / shelf_a0,
111 2.0 * (shelf_k * shelf_k - vh) / shelf_a0,
112 (vh - vb * shelf_k / shelf_q + shelf_k * shelf_k) / shelf_a0,
113 2.0 * (shelf_k * shelf_k - 1.0) / shelf_a0,
114 (1.0 - shelf_k / shelf_q + shelf_k * shelf_k) / shelf_a0},
115 {1.0, -2.0, 1.0,
116 2.0 * (high_pass_k * high_pass_k - 1.0) / high_pass_a0,
117 (1.0 - high_pass_k / high_pass_q + high_pass_k * high_pass_k) / high_pass_a0},
118 true};
119}
120
123 float peak = 0.0f; // Sample peak (linear, 0–1+)
124 float rms = 0.0f; // RMS (linear, 0–1+)
125 float lufs_momentary = -std::numeric_limits<float>::infinity(); // This channel's weighted 400 ms contribution
126 bool clipped = false; // True if any sample >= 1.0
127};
128
131 std::array<ChannelLevels, kMaxMeterChannels> channels{};
133 float correlation = 0.0f; // Stereo correlation (-1 to +1), valid when num_channels >= 2
134 float lufs_integrated = -std::numeric_limits<float>::infinity(); // BS.1770 gated, start to current block
135 // Appended after the legacy aggregate fields so positional aggregate
136 // initialization continues to bind its fourth value to lufs_integrated.
137 float lufs_momentary = -std::numeric_limits<float>::infinity(); // BS.1770 K-weighted, 400 ms
138 float short_term_lufs = -std::numeric_limits<float>::infinity(); // EBU 3 s, 100 ms hop
139 bool short_term_valid = false;
140 float loudness_range_lu = 0.0f;
142};
143
146 struct Channel {
147 float display_peak = 0.0f;
148 float display_rms = 0.0f;
149 float held_peak = 0.0f;
150 float hold_counter = 0.0f;
151 bool clip_indicator = false;
152 float clip_hold_counter = 0.0f;
153 };
154
155 std::array<Channel, kMaxMeterChannels> channels{};
157
158 float attack_time = 0.001f; // seconds
159 float release_time = 0.3f; // seconds
160 float peak_hold_time = 1.5f; // seconds
161 float clip_hold_time = 3.0f; // seconds
162
173 [[nodiscard]] bool update(const MultiChannelMeterData& data, float dt) {
174 const int requested = std::clamp(data.num_channels, 0, kMaxMeterChannels);
175 bool changed = requested != num_channels;
176 num_channels = requested;
177
178 float attack_coeff = 1.0f - std::exp(-dt / attack_time);
179 float release_coeff = 1.0f - std::exp(-dt / release_time);
180
181 for (int ch = 0; ch < num_channels; ++ch) {
182 auto& b = channels[ch];
183 auto& d = data.channels[ch];
184 const float prev_peak = b.display_peak;
185 const float prev_rms = b.display_rms;
186 const float prev_held = b.held_peak;
187 const bool prev_clip = b.clip_indicator;
188
189 // ONE NON-FINITE SAMPLE WOULD DISABLE THE GATE PERMANENTLY. A NaN
190 // takes the release branch, poisons the display value, survives the
191 // snaps below (every comparison against NaN is false), and then
192 // compares unequal to itself forever — so `changed` would be true on
193 // every frame for the rest of the process. Sanitise the input, and
194 // self-heal state that arrived non-finite by another route.
195 const float in_peak = std::isfinite(d.peak) ? d.peak : 0.0f;
196 const float in_rms = std::isfinite(d.rms) ? d.rms : 0.0f;
197 if (!std::isfinite(b.display_peak)) b.display_peak = 0.0f;
198 if (!std::isfinite(b.display_rms)) b.display_rms = 0.0f;
199 if (!std::isfinite(b.held_peak)) b.held_peak = 0.0f;
200
201 // Peak
202 if (in_peak > b.display_peak)
203 b.display_peak += (in_peak - b.display_peak) * attack_coeff;
204 else
205 b.display_peak += (in_peak - b.display_peak) * release_coeff;
206
207 // RMS
208 if (in_rms > b.display_rms)
209 b.display_rms += (in_rms - b.display_rms) * attack_coeff;
210 else
211 b.display_rms += (in_rms - b.display_rms) * release_coeff;
212
213 // Peak hold
214 if (in_peak >= b.held_peak) {
215 b.held_peak = in_peak;
216 b.hold_counter = peak_hold_time;
217 } else {
218 // Floor the counters at zero. Left to run they decrement without
219 // bound for as long as the meter is polled, which is forever.
220 b.hold_counter = std::max(0.0f, b.hold_counter - dt);
221 if (b.hold_counter <= 0)
222 b.held_peak += (0.0f - b.held_peak) * release_coeff;
223 }
224
225 // Clip indicator
226 if (d.clipped) {
227 b.clip_indicator = true;
228 b.clip_hold_counter = clip_hold_time;
229 } else {
230 b.clip_hold_counter = std::max(0.0f, b.clip_hold_counter - dt);
231 if (b.clip_hold_counter <= 0)
232 b.clip_indicator = false;
233 }
234
235 // Clamp noise floor. The decays are exponential, so each of these
236 // approaches zero without ever arriving: unsnapped they change by a
237 // fraction of a bit every frame FOREVER, which is invisible on
238 // screen but means the meter never reports a still frame.
239 //
240 // These three rest at ZERO, so a floor test is enough. A smoother
241 // whose resting value is NOT zero needs the general form —
242 // `if (std::abs(v - target) < eps) v = target` — because a floor
243 // test never fires for it. Copying these three lines to such a
244 // smoother silently does nothing; see CorrelationMeter::update.
245 if (b.display_peak < 1e-6f) b.display_peak = 0;
246 if (b.display_rms < 1e-6f) b.display_rms = 0;
247 if (b.held_peak < 1e-6f) b.held_peak = 0;
248
249 changed = changed || b.display_peak != prev_peak ||
250 b.display_rms != prev_rms || b.held_peak != prev_held ||
251 b.clip_indicator != prev_clip;
252 }
253 return changed;
254 }
255
257 void clear_clips() {
258 for (auto& ch : channels) {
259 ch.clip_indicator = false;
260 ch.clip_hold_counter = 0;
261 }
262 }
263};
264
292template <typename SampleType = float>
294public:
295 void prepare(double sample_rate, int num_channels) {
296 prepare(sample_rate, num_channels, nullptr);
297 }
298
303 void prepare(double sample_rate, int num_channels, const LoudnessChannelRole* roles,
305 features_ = features;
306 prepare_with_features(sample_rate, num_channels, roles);
307 }
308
310 MeterFeatures features() const noexcept {
311 return features_;
312 }
313
319 void prepare(double sample_rate, int num_channels,
320 const LoudnessChannelRole* roles) {
321 features_ = MeterFeatures::all;
322 prepare_with_features(sample_rate, num_channels, roles);
323 }
324
325 private:
326 void prepare_with_features(double sample_rate, int num_channels,
327 const LoudnessChannelRole* roles) {
328 constexpr double maximum_safe_sample_rate =
329 static_cast<double>(std::numeric_limits<int>::max());
330 const bool sample_rate_supported = std::isfinite(sample_rate)
331 && sample_rate > 0.0
332 && sample_rate <= maximum_safe_sample_rate;
333 sample_rate_ = sample_rate_supported ? sample_rate : 0.0;
334 const bool channel_count_supported = num_channels > 0
335 && num_channels <= kMaxMeterChannels;
336 num_channels_ = std::clamp(num_channels, 0, kMaxMeterChannels);
337 loudness_valid_ = has_feature(features_, MeterFeatures::loudness) &&
338 sample_rate_supported && channel_count_supported &&
339 configure_channel_roles(roles);
340 loudness_hop_samples_ = loudness_valid_
341 ? std::max(1, static_cast<int>(std::llround(sample_rate_ * 0.1))) : 0;
342
343 configure_k_weighting();
344 gate_energy_tree_.assign(kGateBinCount + 1, 0.0);
345 gate_count_tree_.assign(kGateBinCount + 1, 0);
346 gate_node_epoch_.assign(kGateBinCount + 1, 0);
347 lra_count_tree_.assign(kGateBinCount + 1, 0);
348 lra_node_epoch_.assign(kGateBinCount + 1, 0);
349 gate_epoch_ = 0;
350 gate_epoch_active_ = true;
351
352 reset_measurement_state(num_channels_);
353 }
354
355 public:
358 void process(const SampleType* const* channels, int num_channels, int num_samples) {
359 const int requested_num_channels = num_channels;
360 num_channels = std::clamp(num_channels, 0, num_channels_);
361 if (channels == nullptr || num_samples <= 0) return;
362
363 for (int ch = 0; ch < num_channels; ++ch) {
364 if (channels[ch] == nullptr) {
365 num_channels = ch;
366 break;
367 }
368 }
369
370 const bool process_loudness = loudness_valid_
371 && requested_num_channels == num_channels_
372 && num_channels == num_channels_;
373 // A topology or loudness-eligibility transition starts a new
374 // measurement programme. The raw requested count participates in the
375 // eligibility check before safe level-meter clamping, so an oversized
376 // layout cannot masquerade as the prepared layout.
377 if (num_channels != active_num_channels_
378 || process_loudness != active_loudness_processing_)
379 reset_measurement_state(num_channels, process_loudness);
380
381 const bool correlate =
382 num_channels >= 2 && has_feature(features_, MeterFeatures::correlation);
383 int offset = 0;
384 while (offset < num_samples) {
385 int piece = num_samples - offset;
386 if (process_loudness)
387 piece = std::min(piece, loudness_hop_samples_ - loudness_hop_position_);
388 if (!measure_piece(channels, num_channels, offset, piece, correlate, process_loudness))
389 measure_piece_sanitized(channels, num_channels, offset, piece, correlate,
390 process_loudness);
391 offset += piece;
392 block_samples_ += piece;
393 if (correlate)
394 correlation_samples_ += piece;
395 if (process_loudness) {
396 loudness_hop_position_ += piece;
397 if (loudness_hop_position_ == loudness_hop_samples_)
398 finish_loudness_hop(num_channels);
399 }
400 }
401
402 // Emit snapshot when we have enough samples for a meaningful measurement
403 // Use ~10ms blocks for responsive metering
404 int block_size = static_cast<int>(sample_rate_ * 0.01);
405 if (block_size < 1) block_size = 1;
406
407 if (block_samples_ >= block_size) {
408 emit_snapshot(num_channels);
409 }
410 }
411
413 const MultiChannelMeterData& snapshot() const { return snapshot_; }
414
417 bool loudness_supported() const noexcept { return loudness_valid_; }
418
419 void reset() {
420 reset_measurement_state(0);
421 }
422
423private:
424 friend struct detail::MultiChannelMeterTestAccess;
425
426 using D2 = detail::lanes::D2;
427 using F4 = detail::lanes::F4;
428
429 // A normalized K-weighting section.
430 struct Section {
431 double b0 = 1.0, b1 = 0.0, b2 = 0.0, a1 = 0.0, a2 = 0.0;
432
433 static Section from(const LoudnessBiquadCoefficients& c) {
434 return {c.b0, c.b1, c.b2, c.a1, c.a2};
435 }
436 };
437
438 // One piece's level readings for one channel.
439 struct PieceLevels {
440 double peak = 0.0;
441 double energy = 0.0;
442 };
443
444 // Vector pass over one channel's piece: peak, sum of squares (in double),
445 // and whether every sample is finite and inside the supported magnitude.
446 // Returns false at the first piece that is not, leaving the caller to
447 // measure it sample by sample.
448 static bool measure_levels(const SampleType* x, int n, PieceLevels& out) {
449 namespace lanes = detail::lanes;
450 int k = 0;
451 double peak = 0.0;
452 double energy = 0.0;
453 if constexpr (std::is_same_v<SampleType, float>) {
454 const F4 limit = lanes::splat(static_cast<float>(kMaxMeterInputMagnitude));
455 F4 peak4 = lanes::splat(0.0f);
456 auto in_range = lanes::less_equal(peak4, limit);
457 D2 e0 = lanes::splat(0.0), e1 = lanes::splat(0.0);
458 for (; k + 4 <= n; k += 4) {
459 const F4 v = lanes::load(x + k);
460 const F4 a = lanes::abs(v);
461 in_range = lanes::both(in_range, lanes::less_equal(a, limit));
462 peak4 = lanes::max(peak4, a);
463 const D2 lo = lanes::widen_low(v);
464 const D2 hi = lanes::widen_high(v);
465 e0 = e0 + lo * lo;
466 e1 = e1 + hi * hi;
467 }
468 if (!lanes::all(in_range))
469 return false;
470 for (int lane = 0; lane < 4; ++lane)
471 peak = std::max(peak, static_cast<double>(lanes::lane(peak4, lane)));
472 energy = (lanes::lane(e0, 0) + lanes::lane(e1, 0)) +
473 (lanes::lane(e0, 1) + lanes::lane(e1, 1));
474 }
475 for (; k < n; ++k) {
476 const double v = static_cast<double>(x[k]);
477 const double a = std::abs(v);
478 if (!(a <= kMaxMeterInputMagnitude))
479 return false;
480 peak = std::max(peak, a);
481 energy += v * v;
482 }
483 out.peak = peak;
484 out.energy = energy;
485 return true;
486 }
487
488 // Sum of x[k] * y[k] in double, for a piece already known to be clean.
489 static double cross_energy(const SampleType* x, const SampleType* y, int n) {
490 namespace lanes = detail::lanes;
491 int k = 0;
492 double sum = 0.0;
493 if constexpr (std::is_same_v<SampleType, float>) {
494 D2 s0 = lanes::splat(0.0), s1 = lanes::splat(0.0);
495 for (; k + 4 <= n; k += 4) {
496 const F4 a = lanes::load(x + k);
497 const F4 b = lanes::load(y + k);
498 s0 = s0 + lanes::widen_low(a) * lanes::widen_low(b);
499 s1 = s1 + lanes::widen_high(a) * lanes::widen_high(b);
500 }
501 sum = (lanes::lane(s0, 0) + lanes::lane(s1, 0)) +
502 (lanes::lane(s0, 1) + lanes::lane(s1, 1));
503 }
504 for (; k < n; ++k)
505 sum += static_cast<double>(x[k]) * static_cast<double>(y[k]);
506 return sum;
507 }
508
509 // Measure a piece with the vector kernels. Returns false, having changed
510 // nothing, when any sample in it needs sanitizing.
511 bool measure_piece(const SampleType* const* channels, int num_channels, int offset, int n,
512 bool correlate, bool loudness) {
513 std::array<PieceLevels, kMaxMeterChannels> levels{};
514 for (int ch = 0; ch < num_channels; ++ch)
515 if (!measure_levels(channels[ch] + offset, n, levels[static_cast<std::size_t>(ch)]))
516 return false;
517 for (int ch = 0; ch < num_channels; ++ch) {
518 const auto& l = levels[static_cast<std::size_t>(ch)];
519 block_peak_[ch] = std::max(block_peak_[ch], l.peak);
520 if (l.peak >= 1.0)
521 block_clipped_[ch] = true;
522 block_sum_sq_[ch] += l.energy;
523 }
524 if (correlate) {
525 correlation_sum_xy_ += cross_energy(channels[0] + offset, channels[1] + offset, n);
526 correlation_sum_xx_ += levels[0].energy;
527 correlation_sum_yy_ += levels[1].energy;
528 }
529 if (loudness)
530 k_weight_piece(channels, num_channels, offset, n);
531 return true;
532 }
533
534 // The per-sample definition: every sample sanitized, then measured.
535 void measure_piece_sanitized(const SampleType* const* channels, int num_channels, int offset,
536 int n, bool correlate, bool loudness) {
537 for (int i = offset; i < offset + n; ++i) {
538 for (int ch = 0; ch < num_channels; ++ch) {
539 // Non-finite samples (NaN/Inf) would poison RMS/LUFS/integrated
540 // accumulators irrecoverably. Treat them as silence.
541 const double sd = sanitize_sample(channels[ch][i]);
542 const double abs_s = std::abs(sd);
543 if (abs_s > block_peak_[ch])
544 block_peak_[ch] = abs_s;
545 if (abs_s >= 1.0)
546 block_clipped_[ch] = true;
547 block_sum_sq_[ch] += sd * sd;
548 if (loudness) {
549 const double weighted = k_weight(ch, sd);
550 loudness_hop_energy_[ch] += weighted * weighted;
551 }
552 }
553 if (correlate) {
554 const double ld = sanitize_sample(channels[0][i]);
555 const double rd = sanitize_sample(channels[1][i]);
556 correlation_sum_xy_ += ld * rd;
557 correlation_sum_xx_ += ld * ld;
558 correlation_sum_yy_ += rd * rd;
559 }
560 }
561 }
562
563 // K-weighting for a clean piece: channels in pairs, each pair one
564 // two-lane vector through both sections, accumulating its weighted
565 // energy into two alternating sums. An odd last channel runs alone.
566 void k_weight_piece(const SampleType* const* channels, int num_channels, int offset, int n) {
567 namespace lanes = detail::lanes;
568 int ch = 0;
569 for (; ch + 2 <= num_channels; ch += 2) {
570 const SampleType* a = channels[ch] + offset;
571 const SampleType* b = channels[ch + 1] + offset;
572 const auto u = static_cast<std::size_t>(ch);
573 auto pair = [&](const std::array<double, kMaxMeterChannels>& v) {
574 return lanes::make(v[u], v[u + 1]);
575 };
576 D2 x1 = pair(k_x1_), x2 = pair(k_x2_);
577 D2 y1 = pair(k_y1_), y2 = pair(k_y2_);
578 D2 w1 = pair(k_w1_), w2 = pair(k_w2_);
579 D2 even = lanes::splat(0.0), odd = lanes::splat(0.0);
580 const Section& sc = shelf_;
581 const Section& hc = high_pass_;
582 int k = 0;
583 auto step = [&](D2 x) {
584 const D2 y = section(sc, x, x1, x2, y1, y2);
585 const D2 w = section(hc, y, y1, y2, w1, w2);
586 x2 = x1;
587 x1 = x;
588 y2 = y1;
589 y1 = y;
590 w2 = w1;
591 w1 = w;
592 return w;
593 };
594 for (; k + 2 <= n; k += 2) {
595 const D2 w0 =
596 step(lanes::make(static_cast<double>(a[k]), static_cast<double>(b[k])));
597 even = even + w0 * w0;
598 const D2 w1k =
599 step(lanes::make(static_cast<double>(a[k + 1]), static_cast<double>(b[k + 1])));
600 odd = odd + w1k * w1k;
601 }
602 for (; k < n; ++k) {
603 const D2 w0 =
604 step(lanes::make(static_cast<double>(a[k]), static_cast<double>(b[k])));
605 even = even + w0 * w0;
606 }
607 const D2 energy = even + odd;
608 store_pair(k_x1_, u, x1);
609 store_pair(k_x2_, u, x2);
610 store_pair(k_y1_, u, y1);
611 store_pair(k_y2_, u, y2);
612 store_pair(k_w1_, u, w1);
613 store_pair(k_w2_, u, w2);
614 loudness_hop_energy_[u] += lanes::lane(energy, 0);
615 loudness_hop_energy_[u + 1] += lanes::lane(energy, 1);
616 }
617 for (; ch < num_channels; ++ch) {
618 const SampleType* a = channels[ch] + offset;
619 double energy = 0.0;
620 for (int k = 0; k < n; ++k) {
621 const double w = k_weight(ch, static_cast<double>(a[k]));
622 energy += w * w;
623 }
624 loudness_hop_energy_[ch] += energy;
625 }
626 }
627
628 // One K-weighting section in direct form I, evaluated so the recursion is
629 // one multiply-add per sample: the feed-forward sum and the older output
630 // term do not depend on the newest output.
631 template <typename T> static T section(const Section& c, T x, T x1, T x2, T y1, T y2) {
632 if constexpr (std::is_same_v<T, double>) {
633 const double head = c.b0 * x + c.b1 * x1 + c.b2 * x2 - c.a2 * y2;
634 return head - c.a1 * y1;
635 } else {
636 namespace lanes = detail::lanes;
637 const T head = lanes::splat(c.b0) * x + lanes::splat(c.b1) * x1 +
638 lanes::splat(c.b2) * x2 - lanes::splat(c.a2) * y2;
639 return head - lanes::splat(c.a1) * y1;
640 }
641 }
642
643 static void store_pair(std::array<double, kMaxMeterChannels>& v, std::size_t u, D2 x) {
644 v[u] = detail::lanes::lane(x, 0);
645 v[u + 1] = detail::lanes::lane(x, 1);
646 }
647
648 void reset_measurement_state(int active_channels,
649 bool process_loudness = false) {
650 for (int ch = 0; ch < kMaxMeterChannels; ++ch) {
651 block_peak_[ch] = 0.0f;
652 block_sum_sq_[ch] = 0.0f;
653 block_clipped_[ch] = false;
654 }
655 block_samples_ = 0;
656 correlation_sum_xy_ = 0.0;
657 correlation_sum_xx_ = 0.0;
658 correlation_sum_yy_ = 0.0;
659 correlation_samples_ = 0;
660 reset_loudness_state();
661 snapshot_ = {};
662 active_num_channels_ = active_channels;
663 active_loudness_processing_ = process_loudness;
664 snapshot_.num_channels = active_channels;
665 }
666
667 void reset_correlation_accumulators() {
668 correlation_sum_xy_ = 0.0;
669 correlation_sum_xx_ = 0.0;
670 correlation_sum_yy_ = 0.0;
671 correlation_samples_ = 0;
672 }
673
674 void emit_snapshot(int num_channels) {
675 snapshot_.num_channels = num_channels;
676
677 for (int ch = 0; ch < num_channels; ++ch) {
678 auto& out = snapshot_.channels[ch];
679 out.peak = static_cast<float>(block_peak_[ch]);
680 out.rms = block_samples_ > 0
681 ? static_cast<float>(std::sqrt(block_sum_sq_[ch] / block_samples_))
682 : 0.0f;
683 out.clipped = block_clipped_[ch];
684
685 out.lufs_momentary = channel_momentary_[ch];
686
687 // Reset block accumulators
688 block_peak_[ch] = 0.0f;
689 block_sum_sq_[ch] = 0.0f;
690 block_clipped_[ch] = false;
691 }
692
693 // Stereo correlation
694 if (num_channels >= 2 && correlation_samples_ > 0) {
695 double denom = std::sqrt(correlation_sum_xx_ * correlation_sum_yy_);
696 snapshot_.correlation = denom > 1e-10
697 ? static_cast<float>(correlation_sum_xy_ / denom)
698 : 0.0f;
699 } else {
700 snapshot_.correlation = 0.0f;
701 }
702
703 // Reset correlation accumulators periodically (every ~100ms)
704 int corr_window = static_cast<int>(sample_rate_ * 0.1);
705 if (correlation_samples_ >= corr_window) {
706 reset_correlation_accumulators();
707 }
708
709 block_samples_ = 0;
710 }
711
712 static constexpr double kAbsoluteGateLufs = -70.0;
713 // The input ceiling above bounds any supported programme below
714 // +140 LUFS, including K-weighting gain and surround weights. 0.01 LU is
715 // comfortably inside the 0.1 LU EBU minimum-requirements tolerance while
716 // keeping each prepared histogram near 0.5 MiB including epoch stamps.
717 static constexpr double kGateMaximumLufs = 140.0;
718 static constexpr double kGateBinWidthLu = 0.01;
719 static constexpr std::size_t kGateBinCount = 21001;
720 static constexpr double kGateMaximumEnergy = 1.0e14;
721 static constexpr std::size_t kShortTermHopCount = 30;
722
723 // One channel, one sample, through both K-weighting sections — the same
724 // arithmetic k_weight_piece() runs on channel pairs.
725 double k_weight(int channel, double x) {
726 const auto c = static_cast<std::size_t>(channel);
727 const double y = section(shelf_, x, k_x1_[c], k_x2_[c], k_y1_[c], k_y2_[c]);
728 const double w = section(high_pass_, y, k_y1_[c], k_y2_[c], k_w1_[c], k_w2_[c]);
729 k_x2_[c] = k_x1_[c];
730 k_x1_[c] = x;
731 k_y2_[c] = k_y1_[c];
732 k_y1_[c] = y;
733 k_w2_[c] = k_w1_[c];
734 k_w1_[c] = w;
735 return w;
736 }
737
738 static double sanitize_sample(SampleType sample) {
739 const double value = static_cast<double>(sample);
740 if (!std::isfinite(value)) return 0.0;
741 return std::clamp(value, -kMaxMeterInputMagnitude,
743 }
744
745 static float energy_to_lufs(double energy) {
746 return energy > 0.0 && std::isfinite(energy)
747 ? static_cast<float>(-0.691 + 10.0 * std::log10(energy))
748 : -std::numeric_limits<float>::infinity();
749 }
750
751 static bool role_weight(LoudnessChannelRole role, double& weight) noexcept {
752 switch (role) {
754 weight = 0.0;
755 return true;
758 weight = 1.41;
759 return true;
765 weight = 1.0;
766 return true;
768 return false;
769 }
770 return false;
771 }
772
773 bool configure_channel_roles(const LoudnessChannelRole* roles) noexcept {
774 channel_weights_.fill(0.0);
775 if (num_channels_ < 1) return false;
776
777 constexpr std::array<LoudnessChannelRole, 8> eight_channel_default{
782 std::array<LoudnessChannelRole, kMaxMeterChannels> configured{};
783
784 if (roles) {
785 for (int ch = 0; ch < num_channels_; ++ch) {
786 const auto role = roles[ch];
787 configured[static_cast<std::size_t>(ch)] = role;
788 }
789 } else {
790 switch (num_channels_) {
791 case 1:
793 break;
794 case 2:
797 break;
798 case 3:
799 std::copy_n(eight_channel_default.begin(), 3, configured.begin());
800 break;
801 case 4:
805 break;
806 case 5:
811 break;
812 case 6:
813 std::copy_n(eight_channel_default.begin(), 6, configured.begin());
814 break;
815 case 8:
816 std::copy(eight_channel_default.begin(),
817 eight_channel_default.end(), configured.begin());
818 break;
819 default:
820 return false;
821 }
822 }
823
824 bool has_program_channel = false;
825 for (int ch = 0; ch < num_channels_; ++ch) {
826 double weight = 0.0;
827 if (!role_weight(configured[static_cast<std::size_t>(ch)], weight)) return false;
828 channel_weights_[static_cast<std::size_t>(ch)] = weight;
829 has_program_channel = has_program_channel || weight > 0.0;
830 }
831 return has_program_channel;
832 }
833
834 void configure_k_weighting() {
835 shelf_ = {};
836 high_pass_ = {};
837 reset_k_weighting_state();
838 const auto coefficients = k_weighting_coefficients(sample_rate_);
839 if (!loudness_valid_ || !coefficients.valid) {
840 loudness_valid_ = false;
841 return;
842 }
843 shelf_ = Section::from(coefficients.shelf);
844 high_pass_ = Section::from(coefficients.high_pass);
845 }
846
847 void reset_k_weighting_state() {
848 for (auto* v : {&k_x1_, &k_x2_, &k_y1_, &k_y2_, &k_w1_, &k_w2_})
849 v->fill(0.0);
850 }
851
852 void reset_loudness_state() {
853 reset_k_weighting_state();
854 loudness_hop_energy_.fill(0.0);
855 for (auto& hop : loudness_energy_ring_) hop.fill(0.0);
856 loudness_short_term_ring_.fill(0.0);
857 channel_momentary_.fill(-std::numeric_limits<float>::infinity());
858 loudness_hop_position_ = 0;
859 loudness_hops_completed_ = 0;
860 loudness_ring_position_ = 0;
861 loudness_short_term_position_ = 0;
862 snapshot_.lufs_momentary = -std::numeric_limits<float>::infinity();
863 snapshot_.lufs_integrated = -std::numeric_limits<float>::infinity();
864 snapshot_.short_term_lufs = -std::numeric_limits<float>::infinity();
865 snapshot_.short_term_valid = false;
866 snapshot_.loudness_range_lu = 0.0f;
867 snapshot_.loudness_range_valid = false;
868 gate_nodes_initialized_ = 0;
869 // A 64-bit epoch cannot wrap in a practical process lifetime. If it
870 // nevertheless exhausts, stop accumulating integrated loudness until
871 // the control thread calls prepare(); never bulk-clear on process().
872 if (gate_epoch_ == std::numeric_limits<std::uint64_t>::max())
873 gate_epoch_active_ = false;
874 else
875 ++gate_epoch_;
876 }
877
878 void finish_loudness_hop(int num_channels) {
879 double hop_program_energy = 0.0;
880 for (int ch = 0; ch < num_channels; ++ch)
881 hop_program_energy += channel_weights_[ch] * loudness_hop_energy_[ch]
882 / static_cast<double>(loudness_hop_samples_);
883 loudness_short_term_ring_[loudness_short_term_position_] = std::min(
884 hop_program_energy, kGateMaximumEnergy);
885 loudness_short_term_position_ = (loudness_short_term_position_ + 1) % kShortTermHopCount;
886 loudness_energy_ring_[loudness_ring_position_] = loudness_hop_energy_;
887 loudness_hop_energy_.fill(0.0);
888 loudness_hop_position_ = 0;
889 loudness_ring_position_ = (loudness_ring_position_ + 1) % 4;
890 ++loudness_hops_completed_;
891 if (loudness_hops_completed_ < 4) return;
892
893 double program_energy = 0.0;
894 for (int ch = 0; ch < num_channels; ++ch) {
895 double channel_energy = 0.0;
896 for (const auto& hop : loudness_energy_ring_) channel_energy += hop[ch];
897 channel_energy /= static_cast<double>(4 * loudness_hop_samples_);
898 const double weighted_energy = std::min(
899 channel_weights_[ch] * channel_energy, kGateMaximumEnergy);
900 channel_momentary_[ch] = energy_to_lufs(weighted_energy);
901 program_energy = std::min(
902 program_energy + weighted_energy, kGateMaximumEnergy);
903 }
904 snapshot_.lufs_momentary = energy_to_lufs(program_energy);
905
906 if (energy_to_lufs(program_energy) > kAbsoluteGateLufs) {
907 add_gating_block(program_energy);
908 update_integrated_loudness();
909 }
910 update_short_term_and_range();
911 }
912
913 void add_gating_block(double energy) {
914 if (!gate_epoch_active_) return;
915 const double loudness = energy_to_lufs(energy);
916 const double clamped = std::clamp(loudness, kAbsoluteGateLufs, kGateMaximumLufs);
917 const auto bin = std::min(kGateBinCount - 1, static_cast<std::size_t>(
918 (clamped - kAbsoluteGateLufs) / kGateBinWidthLu));
919 for (std::size_t index = bin + 1; index <= kGateBinCount;
920 index += low_bit(index)) {
921 if (gate_node_epoch_[index] != gate_epoch_) {
922 gate_node_epoch_[index] = gate_epoch_;
923 gate_energy_tree_[index] = 0.0;
924 gate_count_tree_[index] = 0;
925 ++gate_nodes_initialized_;
926 }
927 gate_energy_tree_[index] += energy;
928 ++gate_count_tree_[index];
929 }
930 }
931
932 std::pair<double, std::uint64_t> gating_blocks_from(std::size_t first_bin) const {
933 if (!gate_epoch_active_) return {0.0, 0};
934 double prefix_energy = 0.0;
935 std::uint64_t prefix_count = 0;
936 for (std::size_t index = first_bin; index > 0; index -= low_bit(index)) {
937 if (gate_node_epoch_[index] == gate_epoch_) {
938 prefix_energy += gate_energy_tree_[index];
939 prefix_count += gate_count_tree_[index];
940 }
941 }
942 double total_energy = 0.0;
943 std::uint64_t total_count = 0;
944 for (std::size_t index = kGateBinCount; index > 0; index -= low_bit(index)) {
945 if (gate_node_epoch_[index] == gate_epoch_) {
946 total_energy += gate_energy_tree_[index];
947 total_count += gate_count_tree_[index];
948 }
949 }
950 return {total_energy - prefix_energy, total_count - prefix_count};
951 }
952
953 static constexpr std::size_t low_bit(std::size_t value) {
954 return value & (~value + 1);
955 }
956
957 void update_integrated_loudness() {
958 const auto [absolute_sum, absolute_count] = gating_blocks_from(0);
959 if (absolute_count == 0) {
960 snapshot_.lufs_integrated = -std::numeric_limits<float>::infinity();
961 return;
962 }
963 const double relative_gate = energy_to_lufs(absolute_sum / absolute_count) - 10.0;
964 const auto first_bin = relative_gate <= kAbsoluteGateLufs ? 0
965 : std::min(kGateBinCount, static_cast<std::size_t>(
966 (relative_gate - kAbsoluteGateLufs) / kGateBinWidthLu) + 1);
967 const auto [gated_sum, gated_count] = gating_blocks_from(first_bin);
968 snapshot_.lufs_integrated = gated_count > 0
969 ? energy_to_lufs(gated_sum / gated_count)
970 : -std::numeric_limits<float>::infinity();
971 }
972
973 void update_short_term_and_range() {
974 if (loudness_hops_completed_ < kShortTermHopCount) return;
975 double energy = 0.0;
976 for (const double hop : loudness_short_term_ring_) energy += hop;
977 snapshot_.short_term_lufs = energy_to_lufs(energy / kShortTermHopCount);
978 snapshot_.short_term_valid = std::isfinite(snapshot_.short_term_lufs);
979 if (!snapshot_.short_term_valid) return;
980 if (snapshot_.short_term_lufs > kAbsoluteGateLufs)
981 add_lra_block(snapshot_.short_term_lufs);
982 update_loudness_range();
983 }
984
985 void add_lra_block(double loudness) {
986 const auto bin = std::min(kGateBinCount - 1, static_cast<std::size_t>(
987 (std::clamp(loudness, kAbsoluteGateLufs, kGateMaximumLufs)
988 - kAbsoluteGateLufs) / kGateBinWidthLu));
989 for (std::size_t index = bin + 1; index <= kGateBinCount; index += low_bit(index)) {
990 if (lra_node_epoch_[index] != gate_epoch_) {
991 lra_node_epoch_[index] = gate_epoch_;
992 lra_count_tree_[index] = 0;
993 }
994 ++lra_count_tree_[index];
995 }
996 }
997
998 std::uint64_t lra_prefix_count(std::size_t exclusive_bin) const {
999 std::uint64_t count = 0;
1000 for (std::size_t index = exclusive_bin; index > 0; index -= low_bit(index))
1001 if (lra_node_epoch_[index] == gate_epoch_) count += lra_count_tree_[index];
1002 return count;
1003 }
1004
1005 float lra_value_at_rank(std::size_t first_bin, std::uint64_t rank) const {
1006 const std::uint64_t target = lra_prefix_count(first_bin) + rank;
1007 std::size_t low = 0, high = kGateBinCount - 1;
1008 while (low < high) {
1009 const auto mid = low + (high - low) / 2;
1010 if (lra_prefix_count(mid + 1) >= target) high = mid;
1011 else low = mid + 1;
1012 }
1013 return static_cast<float>(kAbsoluteGateLufs + low * kGateBinWidthLu);
1014 }
1015
1016 void update_loudness_range() {
1017 if (!std::isfinite(snapshot_.lufs_integrated)) return;
1018 const double gate = std::max(kAbsoluteGateLufs,
1019 static_cast<double>(snapshot_.lufs_integrated) - 20.0);
1020 const auto first_bin = gate <= kAbsoluteGateLufs ? 0 : std::min(kGateBinCount,
1021 static_cast<std::size_t>((gate - kAbsoluteGateLufs) / kGateBinWidthLu) + 1);
1022 const auto count = lra_prefix_count(kGateBinCount) - lra_prefix_count(first_bin);
1023 if (count == 0) return;
1024 const auto p10 = std::max<std::uint64_t>(1, static_cast<std::uint64_t>(std::ceil(count * 0.10)));
1025 const auto p95 = std::max<std::uint64_t>(1, static_cast<std::uint64_t>(std::ceil(count * 0.95)));
1026 snapshot_.loudness_range_lu = lra_value_at_rank(first_bin, p95)
1027 - lra_value_at_rank(first_bin, p10);
1028 snapshot_.loudness_range_valid = true;
1029 }
1030
1031 double sample_rate_ = 44100.0;
1032 int num_channels_ = 2;
1034
1035 // Block accumulators
1036 double block_peak_[kMaxMeterChannels] = {};
1037 double block_sum_sq_[kMaxMeterChannels] = {};
1038 bool block_clipped_[kMaxMeterChannels] = {};
1039 int block_samples_ = 0;
1040
1041 bool loudness_valid_ = false;
1042 int loudness_hop_samples_ = 4410;
1043 int loudness_hop_position_ = 0;
1044 std::uint64_t loudness_hops_completed_ = 0;
1045 std::size_t loudness_ring_position_ = 0;
1046 int active_num_channels_ = 0;
1047 bool active_loudness_processing_ = false;
1048 Section shelf_{};
1049 Section high_pass_{};
1050 // Direct-form-I history per channel: the input (x), the shelf output
1051 // (y, which is also the high-pass input), and the high-pass output (w).
1052 std::array<double, kMaxMeterChannels> k_x1_{};
1053 std::array<double, kMaxMeterChannels> k_x2_{};
1054 std::array<double, kMaxMeterChannels> k_y1_{};
1055 std::array<double, kMaxMeterChannels> k_y2_{};
1056 std::array<double, kMaxMeterChannels> k_w1_{};
1057 std::array<double, kMaxMeterChannels> k_w2_{};
1058 std::array<double, kMaxMeterChannels> channel_weights_{};
1059 std::array<double, kMaxMeterChannels> loudness_hop_energy_{};
1060 std::array<std::array<double, kMaxMeterChannels>, 4> loudness_energy_ring_{};
1061 std::array<double, kShortTermHopCount> loudness_short_term_ring_{};
1062 std::size_t loudness_short_term_position_ = 0;
1063 std::array<float, kMaxMeterChannels> channel_momentary_{};
1064
1065 // Correlation accumulators
1066 double correlation_sum_xy_ = 0.0;
1067 double correlation_sum_xx_ = 0.0;
1068 double correlation_sum_yy_ = 0.0;
1069 int correlation_samples_ = 0;
1070
1071 std::vector<double> gate_energy_tree_;
1072 std::vector<std::uint64_t> gate_count_tree_;
1073 std::vector<std::uint64_t> gate_node_epoch_;
1074 std::vector<std::uint64_t> lra_count_tree_;
1075 std::vector<std::uint64_t> lra_node_epoch_;
1076 std::uint64_t gate_epoch_ = 0;
1077 std::size_t gate_nodes_initialized_ = 0;
1078 bool gate_epoch_active_ = true;
1079
1080 MultiChannelMeterData snapshot_;
1081};
1082
1085
1086} // namespace pulp::signal
Audio-thread metering processor.
Definition multi_channel_meter.hpp:293
void process(const SampleType *const *channels, int num_channels, int num_samples)
Process a block of interleaved or deinterleaved audio.
Definition multi_channel_meter.hpp:358
void prepare(double sample_rate, int num_channels, const LoudnessChannelRole *roles)
Prepare the loudness meter with an explicit speaker layout and every feature (MeterFeatures::all),...
Definition multi_channel_meter.hpp:319
MeterFeatures features() const noexcept
Definition multi_channel_meter.hpp:310
const MultiChannelMeterData & snapshot() const
Get the latest metering snapshot.
Definition multi_channel_meter.hpp:413
void reset()
Definition multi_channel_meter.hpp:419
bool loudness_supported() const noexcept
Whether the prepared sample rate and speaker layout support BS.1770 loudness.
Definition multi_channel_meter.hpp:417
void prepare(double sample_rate, int num_channels, const LoudnessChannelRole *roles, MeterFeatures features)
Prepare with a feature set.
Definition multi_channel_meter.hpp:303
void prepare(double sample_rate, int num_channels)
Definition multi_channel_meter.hpp:295
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
@ x1
Definition forge_distortion_catalog.hpp:48
@ x2
Definition forge_distortion_catalog.hpp:48
@ b
Definition pitch.hpp:31
@ high
Definition gpu_capabilities.hpp:157
@ index
Definition gpu_pass.hpp:140
@ low
Definition background_job.hpp:22
Round-2 effect-wave vocabulary.
Definition analysis_frontends.hpp:50
Definition additive_bank.hpp:218
MultiChannelMeterT< float > MultiChannelMeter
Definition multi_channel_meter.hpp:1083
constexpr MeterFeatures operator|(MeterFeatures a, MeterFeatures b) noexcept
Definition multi_channel_meter.hpp:54
LoudnessChannelRole
Speaker roles used by the BS.1770 channel summation.
Definition multi_channel_meter.hpp:65
@ left_surround
Definition multi_channel_meter.hpp:70
@ right
Definition multi_channel_meter.hpp:67
@ left
Definition multi_channel_meter.hpp:66
@ right_surround
Definition multi_channel_meter.hpp:71
@ unknown
Definition multi_channel_meter.hpp:74
@ center
Definition multi_channel_meter.hpp:68
@ right_rear_surround
Definition multi_channel_meter.hpp:73
@ left_rear_surround
Definition multi_channel_meter.hpp:72
@ lfe
Definition multi_channel_meter.hpp:69
@ peak
Sample peak. Literal, fastest-responding.
Definition feedforward_compressor.hpp:119
@ c
Definition spectrum_trace.hpp:35
@ energy
sum h^2 = 1 per output ear (default; convolution reverb).
Definition zero_latency_convolver_support.hpp:160
static constexpr double kMaxMeterInputMagnitude
Finite samples outside this deliberately generous audio-domain ceiling are clipped before every peak,...
Definition multi_channel_meter.hpp:41
static constexpr int kMaxMeterChannels
Maximum supported channel count for metering.
Definition multi_channel_meter.hpp:35
constexpr bool has_feature(MeterFeatures set, MeterFeatures feature) noexcept
Definition multi_channel_meter.hpp:58
MeterFeatures
What a MultiChannelMeter computes.
Definition multi_channel_meter.hpp:47
@ loudness
Definition multi_channel_meter.hpp:50
@ correlation
Definition multi_channel_meter.hpp:49
@ levels
Definition multi_channel_meter.hpp:48
@ all
Definition multi_channel_meter.hpp:51
@ a
Definition formant_filter_bank.hpp:20
@ u
Definition formant_filter_bank.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
MultiChannelMeterT< double > MultiChannelMeter64
Definition multi_channel_meter.hpp:1084
KWeightingCoefficients k_weighting_coefficients(double sample_rate)
Return the two-stage K-weighting design for a sample rate.
Definition multi_channel_meter.hpp:91
@ configured
Definition formant_filter_bank.hpp:23
float sum(const float *data, std::size_t count) noexcept
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
@ l
Definition input_events.hpp:40
@ y
Definition input_events.hpp:42
@ n
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40
@ x
Definition input_events.hpp:41
@ v
Definition input_events.hpp:41
@ w
Definition input_events.hpp:41
Per-channel level measurements computed on the audio thread.
Definition multi_channel_meter.hpp:122
float rms
Definition multi_channel_meter.hpp:124
float lufs_momentary
Definition multi_channel_meter.hpp:125
bool clipped
Definition multi_channel_meter.hpp:126
float peak
Definition multi_channel_meter.hpp:123
Definition multi_channel_meter.hpp:81
bool valid
Definition multi_channel_meter.hpp:84
LoudnessBiquadCoefficients high_pass
Definition multi_channel_meter.hpp:83
LoudnessBiquadCoefficients shelf
Definition multi_channel_meter.hpp:82
Definition multi_channel_meter.hpp:77
double b1
Definition multi_channel_meter.hpp:78
double a1
Definition multi_channel_meter.hpp:78
double b0
Definition multi_channel_meter.hpp:78
double b2
Definition multi_channel_meter.hpp:78
double a2
Definition multi_channel_meter.hpp:78
Definition multi_channel_meter.hpp:146
float display_peak
Definition multi_channel_meter.hpp:147
float hold_counter
Definition multi_channel_meter.hpp:150
float held_peak
Definition multi_channel_meter.hpp:149
float display_rms
Definition multi_channel_meter.hpp:148
float clip_hold_counter
Definition multi_channel_meter.hpp:152
bool clip_indicator
Definition multi_channel_meter.hpp:151
Configurable ballistics for multi-channel meter display (UI thread).
Definition multi_channel_meter.hpp:145
float peak_hold_time
Definition multi_channel_meter.hpp:160
float attack_time
Definition multi_channel_meter.hpp:158
std::array< Channel, kMaxMeterChannels > channels
Definition multi_channel_meter.hpp:155
bool update(const MultiChannelMeterData &data, float dt)
Update ballistics from new meter data.
Definition multi_channel_meter.hpp:173
float clip_hold_time
Definition multi_channel_meter.hpp:161
float release_time
Definition multi_channel_meter.hpp:159
int num_channels
Definition multi_channel_meter.hpp:156
void clear_clips()
Reset all clip indicators immediately.
Definition multi_channel_meter.hpp:257
Complete metering snapshot for all channels, published lock-free.
Definition multi_channel_meter.hpp:130
bool loudness_range_valid
Definition multi_channel_meter.hpp:141
float lufs_integrated
Definition multi_channel_meter.hpp:134
float lufs_momentary
Definition multi_channel_meter.hpp:137
std::array< ChannelLevels, kMaxMeterChannels > channels
Definition multi_channel_meter.hpp:131
float short_term_lufs
Definition multi_channel_meter.hpp:138
bool short_term_valid
Definition multi_channel_meter.hpp:139
float loudness_range_lu
Definition multi_channel_meter.hpp:140
int num_channels
Definition multi_channel_meter.hpp:132
float correlation
Definition multi_channel_meter.hpp:133