///|
/// Errors returned while combining finalized reports.
pub enum AggregationError {
UnsupportedAggregateSampleRate(Int32)
ReportSampleRateMismatch(Int32, Int32)
AggregateCounterOverflow
InvalidAggregateCounters
InvalidAggregateDistribution
InvalidAggregatePeak
}
///|
/// Fixed-memory accumulator for independently measured programs at one sample rate.
pub struct LoudnessAggregator {
priv sample_rate : Int32
priv integrated_distribution : LoudnessHistogram
priv short_distribution : LoudnessHistogram
priv mut sample_peak_linear : Double
priv mut true_peak_linear : Double
priv mut frame_count : Int32
priv mut asset_count : Int32
}
///|
/// Creates an accumulator compatible with the available coefficient set.
pub fn LoudnessAggregator::new(
sample_rate : Int32,
) -> Result[LoudnessAggregator, AggregationError] {
if sample_rate != 48000 {
Err(UnsupportedAggregateSampleRate(sample_rate))
} else {
Ok({
sample_rate,
integrated_distribution: LoudnessHistogram::new(),
short_distribution: LoudnessHistogram::new(),
sample_peak_linear: 0.0,
true_peak_linear: 0.0,
frame_count: 0,
asset_count: 0,
})
}
}
///|
/// Adds one finalized asset without retaining its report or PCM samples.
pub fn LoudnessAggregator::add(
self : LoudnessAggregator,
report : MeasurementReport,
) -> Result[Unit, AggregationError] {
if report.sample_rate != self.sample_rate {
return Err(ReportSampleRateMismatch(self.sample_rate, report.sample_rate))
}
match
self.validate_addition(
report.frame_count,
1,
report.integrated_distribution,
report.short_distribution,
report.sample_peak_linear,
report.true_peak_linear,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
self.integrated_distribution.merge(report.integrated_distribution)
self.short_distribution.merge(report.short_distribution)
if report.sample_peak_linear > self.sample_peak_linear {
self.sample_peak_linear = report.sample_peak_linear
}
if report.true_peak_linear > self.true_peak_linear {
self.true_peak_linear = report.true_peak_linear
}
self.frame_count = self.frame_count + report.frame_count
self.asset_count = self.asset_count + 1
Ok(())
}
///|
/// Number of finalized reports accepted so far.
pub fn LoudnessAggregator::asset_count(self : LoudnessAggregator) -> Int32 {
self.asset_count
}
///|
pub fn LoudnessAggregator::frame_count(self : LoudnessAggregator) -> Int32 {
self.frame_count
}
///|
/// Immutable combined result calculated over all accepted loudness blocks.
pub struct AggregateReport {
priv sample_rate : Int32
priv asset_count : Int32
priv frame_count : Int32
priv integrated_status : GateStatus
priv lra_status : GateStatus
priv integrated_gate : IntegratedGateReport?
priv lra_gate : LraGateReport?
priv sample_peak_linear : Double
priv true_peak_linear : Double
}
///|
pub fn AggregateReport::sample_rate(self : AggregateReport) -> Int32 {
self.sample_rate
}
///|
pub fn AggregateReport::asset_count(self : AggregateReport) -> Int32 {
self.asset_count
}
///|
pub fn AggregateReport::frame_count(self : AggregateReport) -> Int32 {
self.frame_count
}
///|
pub fn AggregateReport::duration_seconds(self : AggregateReport) -> Double {
self.frame_count.to_double() / self.sample_rate.to_double()
}
///|
pub fn AggregateReport::integrated_lufs(self : AggregateReport) -> Double? {
match self.integrated_gate {
Some(gate) => Some(gate.integrated_lufs())
None => None
}
}
///|
pub fn AggregateReport::loudness_range_lu(self : AggregateReport) -> Double? {
match self.lra_gate {
Some(gate) => Some(gate.loudness_range_lu())
None => None
}
}
///|
pub fn AggregateReport::integrated_gate(
self : AggregateReport,
) -> IntegratedGateReport? {
self.integrated_gate
}
///|
pub fn AggregateReport::lra_gate(self : AggregateReport) -> LraGateReport? {
self.lra_gate
}
///|
pub fn AggregateReport::sample_peak_linear(self : AggregateReport) -> Double {
self.sample_peak_linear
}
///|
pub fn AggregateReport::true_peak_linear(self : AggregateReport) -> Double {
self.true_peak_linear
}
///|
pub fn AggregateReport::sample_peak_dbfs(self : AggregateReport) -> Double? {
amplitude_to_db(self.sample_peak_linear)
}
///|
pub fn AggregateReport::true_peak_dbtp(self : AggregateReport) -> Double? {
amplitude_to_db(self.true_peak_linear)
}
///|
/// Produces a detached aggregate report; the accumulator remains usable afterward.
pub fn LoudnessAggregator::finish(self : LoudnessAggregator) -> AggregateReport {
{
sample_rate: self.sample_rate,
asset_count: self.asset_count,
frame_count: self.frame_count,
integrated_status: self.integrated_status(),
lra_status: self.lra_status(),
integrated_gate: integrated_gate_from_histogram(
self.integrated_distribution,
),
lra_gate: lra_gate_from_histogram(self.short_distribution),
sample_peak_linear: self.sample_peak_linear,
true_peak_linear: self.true_peak_linear,
}
}
///|
pub fn LoudnessAggregator::sample_rate(self : LoudnessAggregator) -> Int32 {
self.sample_rate
}
///|
/// Preflights both distributions and all counters before any state is changed.
/// Counts stay within Int range because public gate reports expose Int counts.
fn LoudnessAggregator::validate_addition(
self : LoudnessAggregator,
frames : Int32,
assets : Int32,
integrated : LoudnessHistogram,
short : LoudnessHistogram,
sample_peak : Double,
true_peak : Double,
) -> Result[Unit, AggregationError] {
if frames < 0 || assets < 0 || self.frame_count < 0 || self.asset_count < 0 {
return Err(InvalidAggregateCounters)
}
if frames > 2147483647 - self.frame_count ||
assets > 2147483647 - self.asset_count {
return Err(AggregateCounterOverflow)
}
if sample_peak < 0.0 ||
true_peak < sample_peak ||
sample_peak - sample_peak != 0.0 ||
true_peak - true_peak != 0.0 {
return Err(InvalidAggregatePeak)
}
match validate_histogram_merge(self.integrated_distribution, integrated) {
Err(error) => return Err(error)
Ok(_) => ()
}
validate_histogram_merge(self.short_distribution, short)
}
///|
fn validate_histogram_merge(
destination : LoudnessHistogram,
source : LoudnessHistogram,
) -> Result[Unit, AggregationError] {
if source.counts.length() != destination.counts.length() ||
source.energy_sums.length() != source.counts.length() {
return Err(InvalidAggregateDistribution)
}
let mut total = 0.0
for bucket in 0.. 2147483647.0 ||
@math.floor(count) != count ||
energy - energy != 0.0 ||
energy < 0.0 ||
(count == 0.0 && energy != 0.0) {
return Err(InvalidAggregateDistribution)
}
total = total + count
let combined_energy = destination.energy_sums[bucket] + energy
if combined_energy - combined_energy != 0.0 {
return Err(AggregateCounterOverflow)
}
}
if source.above_range_count < 0 {
return Err(InvalidAggregateDistribution)
}
total = total + source.above_range_count.to_double()
if total != source.total_count {
return Err(InvalidAggregateDistribution)
}
if total + destination.total_count > 2147483647.0 {
return Err(AggregateCounterOverflow)
}
Ok(())
}
///|
/// Combines independent programs, not concatenated PCM: no cross-program
/// windows or filter transitions are invented. Neither source nor reports are
/// retained. Self-merge deliberately doubles the accepted programs. Histogram
/// addition is associative up to floating-point energy-sum rounding.
pub fn LoudnessAggregator::merge(
self : LoudnessAggregator,
other : LoudnessAggregator,
) -> Result[Unit, AggregationError] {
if other.sample_rate != self.sample_rate {
return Err(ReportSampleRateMismatch(self.sample_rate, other.sample_rate))
}
match
self.validate_addition(
other.frame_count,
other.asset_count,
other.integrated_distribution,
other.short_distribution,
other.sample_peak_linear,
other.true_peak_linear,
) {
Err(error) => return Err(error)
Ok(_) => ()
}
self.integrated_distribution.merge(other.integrated_distribution)
self.short_distribution.merge(other.short_distribution)
self.frame_count = self.frame_count + other.frame_count
self.asset_count = self.asset_count + other.asset_count
if other.sample_peak_linear > self.sample_peak_linear {
self.sample_peak_linear = other.sample_peak_linear
}
if other.true_peak_linear > self.true_peak_linear {
self.true_peak_linear = other.true_peak_linear
}
Ok(())
}
///|
test "aggregation rejects invalid private counters and malformed second histogram atomically" {
let accumulator = LoudnessAggregator::new(48000).unwrap()
let analyzer = Analyzer::new(
MeterConfig::new(48000, ChannelLayout::mono()).unwrap(),
)
let report = analyzer.finish()
assert_true(
accumulator.validate_addition(
-1,
1,
report.integrated_distribution,
report.short_distribution,
0.0,
0.0,
)
is Err(InvalidAggregateCounters),
)
report.integrated_distribution.push_energy(1.0)
report.short_distribution.counts[0] = -1.0
assert_true(accumulator.add(report) is Err(InvalidAggregateDistribution))
assert_eq(accumulator.asset_count(), 0)
assert_eq(accumulator.integrated_distribution.total_count, 0.0)
assert_eq(accumulator.sample_peak_linear, 0.0)
}
///|
test "aggregation histogram count and energy overflow are atomic" {
let accumulator = LoudnessAggregator::new(48000).unwrap()
let other = LoudnessAggregator::new(48000).unwrap()
other.integrated_distribution.counts[0] = 2147483647.0
other.integrated_distribution.total_count = 2147483647.0
assert_true(accumulator.merge(other) is Ok(_))
assert_true(accumulator.merge(other) is Err(AggregateCounterOverflow))
assert_eq(accumulator.integrated_distribution.total_count, 2147483647.0)
let energy = LoudnessAggregator::new(48000).unwrap()
energy.short_distribution.counts[0] = 1.0
energy.short_distribution.total_count = 1.0
energy.short_distribution.energy_sums[0] = 1.0e308
assert_true(energy.merge(energy) is Err(AggregateCounterOverflow))
assert_eq(energy.short_distribution.energy_sums[0], 1.0e308)
}