///| Channel ordering accepted by the analyzer.
///|
/// FivePointZero uses L, R, C, Ls, Rs order; it has no LFE channel.
pub enum ChannelLayout {
Mono
Stereo
FivePointZero
}
///|
/// Returns the layouts implemented by this release.
pub fn ChannelLayout::supported() -> Array[ChannelLayout] {
[Mono, Stereo, FivePointZero]
}
///|
/// Returns the canonical mono layout.
pub fn ChannelLayout::mono() -> ChannelLayout {
Mono
}
///|
/// Returns the canonical left-right stereo layout.
pub fn ChannelLayout::stereo() -> ChannelLayout {
Stereo
}
///|
/// Returns the canonical 5.0 layout in L,R,C,Ls,Rs order.
pub fn ChannelLayout::five_point_zero() -> ChannelLayout {
FivePointZero
}
///|
/// Returns the canonical short name for a channel index in this layout.
pub fn ChannelLayout::channel_label(
self : ChannelLayout,
index : Int32,
) -> String? {
match (self, index) {
(Mono, 0) => Some("C")
(Stereo, 0) => Some("L")
(Stereo, 1) => Some("R")
(FivePointZero, 0) => Some("L")
(FivePointZero, 1) => Some("R")
(FivePointZero, 2) => Some("C")
(FivePointZero, 3) => Some("Ls")
(FivePointZero, 4) => Some("Rs")
_ => None
}
}
///|
/// Returns the BS.1770 channel multiplier, or None for an invalid index.
pub fn ChannelLayout::channel_weight(
self : ChannelLayout,
index : Int32,
) -> Double? {
match (self, index) {
(Mono, 0) | (Stereo, 0) | (Stereo, 1) => Some(1.0)
(FivePointZero, 0) | (FivePointZero, 1) | (FivePointZero, 2) => Some(1.0)
(FivePointZero, 3) | (FivePointZero, 4) => Some(1.41)
_ => None
}
}
///|
/// Finds the exact canonical channel label in a layout.
pub fn ChannelLayout::index_of(self : ChannelLayout, label : String) -> Int32? {
for index in 0.. Int32 {
match self {
Mono => 1
Stereo => 2
FivePointZero => 5
}
}
///|
/// Typed configuration and input errors.
pub enum MeterError {
UnsupportedSampleRate(Int32)
IncompleteFrame(Int32, Int32)
NonFiniteSample
SampleMagnitudeTooLarge(Double)
CounterOverflow
InvalidHistoryCapacity(Int32)
AnalyzerFinished
}
///|
/// Immutable configuration for one 48 kHz PCM stream.
pub struct MeterConfig {
priv sample_rate : Int32
priv layout : ChannelLayout
}
///|
/// Creates a configuration. Only the published 48 kHz coefficient set is enabled.
pub fn MeterConfig::new(
sample_rate : Int32,
layout : ChannelLayout,
) -> Result[MeterConfig, MeterError] {
if sample_rate != 48000 {
Err(UnsupportedSampleRate(sample_rate))
} else {
Ok({ sample_rate, layout, })
}
}
///|
/// Returns the configured sample rate in samples per second.
pub fn MeterConfig::sample_rate(self : MeterConfig) -> Int32 {
self.sample_rate
}
///|
/// Returns the interleaved channel layout.
pub fn MeterConfig::layout(self : MeterConfig) -> ChannelLayout {
self.layout
}
///|
/// Allocates independent K-weighting state for every configured channel.
fn new_filter_states(layout : ChannelLayout) -> Array[KWeightingState] {
let states : Array[KWeightingState] = []
for _ in 0.. Array[Double] {
let ring : Array[Double] = []
for _ in 0.. Array[Array[Double]] {
[
[
0.001708984375, 0.010986328125, -0.0196533203125, 0.033203125, -0.0594482421875,
0.1373291015625, 0.97216796875, -0.102294921875, 0.047607421875, -0.026611328125,
0.014892578125, -0.00830078125,
],
[
-0.0291748046875, 0.029296875, -0.0517578125, 0.089111328125, -0.16650390625,
0.465087890625, 0.77978515625, -0.2003173828125, 0.1015625, -0.0582275390625,
0.0330810546875, -0.0189208984375,
],
[
-0.0189208984375, 0.0330810546875, -0.0582275390625, 0.1015625, -0.2003173828125,
0.77978515625, 0.465087890625, -0.16650390625, 0.089111328125, -0.0517578125,
0.029296875, -0.0291748046875,
],
[
-0.00830078125, 0.014892578125, -0.026611328125, 0.047607421875, -0.102294921875,
0.97216796875, 0.1373291015625, -0.0594482421875, 0.033203125, -0.0196533203125,
0.010986328125, 0.001708984375,
],
]
}
///|
/// Stateful analyzer for interleaved PCM frames at 48 kHz.
pub struct Analyzer {
priv config : MeterConfig
priv mut filters : Array[KWeightingState]
priv mut sample_peak : Double
priv channel_sample_peaks : Array[Double]
priv peak_coefficients : Array[Array[Double]]
priv mut true_peak_ring : Array[Array[Double]]
priv mut true_peak_cursor : Int32
priv mut true_peak_max : Double
priv mut sample_count : Int32
priv mut frame_count : Int32
priv momentary_window : EnergyWindow
priv short_window : EnergyWindow
priv mut latest_momentary_energy : Double?
priv mut latest_short_energy : Double?
priv integrated_distribution : LoudnessHistogram
priv short_distribution : LoudnessHistogram
priv mut finished : Bool
priv mut cached_report : MeasurementReport?
priv history : MeasurementHistory
}
///|
/// Starts an empty analyzer with separate filter and window history.
pub fn Analyzer::new(config : MeterConfig) -> Analyzer {
Analyzer::new_internal(config, 600)
}
///|
/// Creates an analyzer with a bounded history capacity (zero disables retention).
pub fn Analyzer::new_with_history_capacity(
config : MeterConfig,
capacity : Int32,
) -> Result[Analyzer, MeterError] {
if capacity < 0 || capacity > 100000 {
Err(InvalidHistoryCapacity(capacity))
} else {
Ok(Analyzer::new_internal(config, capacity))
}
}
///|
fn Analyzer::new_internal(
config : MeterConfig,
history_capacity : Int32,
) -> Analyzer {
let peak_ring : Array[Array[Double]] = []
for _ in 0.. Result[Unit, MeterError] {
self.push_view(samples[:])
}
///|
/// Consumes a borrowed PCM view synchronously without copying or retaining it.
/// Validation remains atomic; the caller must not mutate the view during this call.
pub fn Analyzer::push_view(
self : Analyzer,
samples : ArrayView[Double],
) -> Result[Unit, MeterError] {
if self.finished {
return Err(AnalyzerFinished)
}
let channels = self.config.layout.channels()
if samples.length() % channels != 0 {
return Err(IncompleteFrame(samples.length(), channels))
}
for sample in samples {
if sample - sample != 0.0 {
return Err(NonFiniteSample)
}
}
let input_frames = samples.length() / channels
if input_frames > 2147483647 - self.frame_count ||
samples.length() > 2147483647 - self.sample_count {
return Err(CounterOverflow)
}
for sample in samples {
if abs_value(sample) > 1.0e+150 {
return Err(SampleMagnitudeTooLarge(sample))
}
}
for frame in 0.. self.sample_peak {
self.sample_peak = magnitude
}
if magnitude > self.channel_sample_peaks[channel] {
self.channel_sample_peaks[channel] = magnitude
}
let weighted = self.filters[channel].process(sample)
let channel_weight = match self.config.layout.channel_weight(channel) {
Some(weight) => weight
None => 0.0
}
frame_energy = frame_energy + weighted * weighted * channel_weight
}
self.push_true_peak_frame(samples, offset, true)
self.sample_count = self.sample_count + channels
self.record_frame_energy(frame_energy)
}
Ok(())
}
///|
/// Absolute value without introducing a dependency on a platform-specific intrinsic.
fn abs_value(value : Double) -> Double {
if value < 0.0 {
0.0 - value
} else {
value
}
}
///|
/// Adds one frame to the FIR delay line and evaluates its four interpolation phases.
fn Analyzer::push_true_peak_frame(
self : Analyzer,
samples : ArrayView[Double],
offset : Int32,
has_input : Bool,
) -> Unit {
let channels = self.config.layout.channels()
for channel in 0.. self.true_peak_max {
self.true_peak_max = magnitude
}
}
}
self.true_peak_cursor = (self.true_peak_cursor + 1) % 12
}
///|
/// Retains window means and samples the two measurement time scales.
fn Analyzer::record_frame_energy(
self : Analyzer,
frame_energy : Double,
) -> Unit {
self.latest_momentary_energy = self.momentary_window.push(frame_energy)
self.latest_short_energy = self.short_window.push(frame_energy)
self.frame_count = self.frame_count + 1
if self.frame_count >= 19200 && (self.frame_count - 19200) % 4800 == 0 {
match self.latest_momentary_energy {
Some(energy) => self.integrated_distribution.push_energy(energy)
None => ()
}
self.record_snapshot()
}
if self.frame_count >= 144000 && (self.frame_count - 144000) % 4800 == 0 {
match self.latest_short_energy {
Some(energy) => self.short_distribution.push_energy(energy)
None => ()
}
}
}
///|
/// Converts weighted mean-square energy to LUFS/LKFS.
fn energy_to_lufs(energy : Double) -> Double {
if energy <= 0.0 {
-1000.0
} else {
-0.691 + 10.0 * @math.log10(energy)
}
}
///|
/// Returns the latest complete 400 ms momentary window when available.
pub fn Analyzer::momentary_lufs(self : Analyzer) -> Double? {
match self.latest_momentary_energy {
Some(energy) => Some(energy_to_lufs(energy))
None => None
}
}
///|
/// Returns the latest complete 3 s short-term window when available.
pub fn Analyzer::short_term_lufs(self : Analyzer) -> Double? {
match self.latest_short_energy {
Some(energy) => Some(energy_to_lufs(energy))
None => None
}
}
///|
/// Returns the maximum Annex 2 four-phase FIR estimate in linear amplitude.
pub fn Analyzer::true_peak_linear(self : Analyzer) -> Double {
if self.sample_peak > self.true_peak_max {
self.sample_peak
} else {
self.true_peak_max
}
}
///|
/// Returns the largest absolute input sample in linear amplitude.
pub fn Analyzer::sample_peak_linear(self : Analyzer) -> Double {
self.sample_peak
}
///|
/// Returns one channel's maximum absolute input sample, or None for an invalid index.
pub fn Analyzer::channel_sample_peak_linear(
self : Analyzer,
channel : Int32,
) -> Double? {
if channel < 0 || channel >= self.channel_sample_peaks.length() {
None
} else {
Some(self.channel_sample_peaks[channel])
}
}
///|
/// Returns a detached copy of the channel-ordered sample-peak array.
pub fn Analyzer::channel_sample_peaks_linear(self : Analyzer) -> Array[Double] {
let result : Array[Double] = []
for peak in self.channel_sample_peaks {
result.push(peak)
}
result
}
///|
/// Returns the number of accepted interleaved samples.
pub fn Analyzer::sample_count(self : Analyzer) -> Int32 {
self.sample_count
}
///|
/// Returns the number of accepted frames.
pub fn Analyzer::frame_count(self : Analyzer) -> Int32 {
self.frame_count
}
///|
/// Returns the analyzed duration based on complete 48 kHz frames.
pub fn Analyzer::duration_seconds(self : Analyzer) -> Double {
self.frame_count.to_double() / self.config.sample_rate.to_double()
}