fix(dsp): BS.1770 integrated loudness misses the 997 Hz calibration anchor - #860
Conversation
integrated_loudness() missed the Recommendation's own calibration point: a 0 dB FS 997 Hz sine on one channel read -3.0517 LKFS instead of the -3.01 LKFS BS.1770 states, because the RLB high-pass numerator was normalised by its denominator where Table 2 specifies exactly [1, -2, 1]. That cost 0.043 dB across the band above ~500 Hz, so the -0.691 constant no longer cancelled the K-weighting gain at 997 Hz. Both stages are now built from the analogue prototypes behind Tables 1 and 2 and reproduce the tabulated 48 kHz coefficients to 1e-15. The gating loop also rounded the block count instead of taking the floor, so a partial final block was measured but divided by a full block length. BS.1770 says incomplete gating blocks at the end are not used. Block indices are now integer sample offsets, which also made the reading of a steady tone independent of clip length (it moved up to 0.19 LU before). The first-pass absolute gate used >= where the Recommendation uses >. The previous tests pinned the old output to abs=1e-12, so they locked the error in rather than catching it; they are replaced by assertions against the Table 1/2 coefficients, the 997 Hz anchor, incomplete-block exclusion, and the two gating thresholds.
| upper = int(block_size * (block_index * step + 1) * rate) | ||
| mean_square[channel, block_index] = (1.0 / (block_size * rate)) * np.sum( | ||
| np.square(input_data[lower:upper, channel]) | ||
| lower = block_index * hop_samples |
There was a problem hiding this comment.
I don't think this is quite correct? hop_samples is getting rounded up front now and will accumulate error at 11.025kHz (aka 44.1kHz / 4) since the hop sizes won't be exactly uniform, right? Can you add a test for 11.025kHz to make sure the hop sizes are correct?
There was a problem hiding this comment.
The hop is now a constant integer, so spacing is exactly uniform — that's the fix, not the problem: the old code derived each boundary from block_size * block_index * step * rate in floats, which is what could drift. At 11.025 kHz the hop is round(1102.5) = 1102 samples (0.1 s to the nearest sample, per BS.1770), and every block starts at an exact integer multiple of it. Added test_integrated_loudness_block_hop_11025hz (7c44e39) asserting the block/hop values, uniform spacing, and the 997 Hz anchor (−26.010 at 48 kHz vs −25.969 at 11.025 kHz, within the per-rate filter redesign tolerance).
There was a problem hiding this comment.
Ok, fair enough, we're splitting hairs a bit and I agree the uniform step is likely ideal so I'm good with this. Thanks!
lucasnewman
left a comment
There was a problem hiding this comment.
Overall this looks reasonable but please take a look at my one comment.
Context
integrated_loudness()misses BS.1770's own calibration point. The Recommendation states that a 0 dB FS 997 Hz sine on one channel "will equal -3.01 LKFS"; onmainit reads -3.0517. Cause: the RLB high-pass numerator is normalised by its denominator, where Table 2 specifies it as exactly[1, -2, 1]. That loses 0.043 dB across the whole band above ~500 Hz, so the-0.691constant no longer cancels the K-weighting gain at 997 Hz (0.6496 dB measured, 0.691 dB required by Note 1).Two further defects in the same path: the gating loop rounds the block count instead of flooring it, so a partial final block is measured but divided by a full block length — BS.1770 says incomplete gating blocks at the end are not used — which makes a steady tone's reading depend on clip length (0.19 LU between a 0.50 s and a 0.55 s tone at 24 kHz). And the first-pass absolute gate uses
>=where equations (6)/(7) use>.Description
Both K-weighting stages are now built from the analogue prototypes behind Tables 1 and 2, so they reproduce the tabulated 48 kHz coefficients to 1e-15 and rescale correctly to other rates; block indices are integer sample offsets and only complete blocks are measured. The 997 Hz anchor then reads -3.0103 LKFS and the reading no longer moves with clip length.
The existing tests pinned the old output to
abs=1e-12, so they locked the error in rather than catching it. They are replaced by assertions against the Table 1/2 coefficients, the 997 Hz anchor, incomplete-block exclusion, and the two gating thresholds.Verified out of tree against
ffmpeg -filter:a ebur128andpyloudnorm(which reaches the same coefficients underfilter_class="DeMan"): EBU Tech 3341 cases 1-5 all land within 0.022 LU, and case 4 equals case 3 exactly, so the -70 LKFS gate is doing its job. Channel weights, gate thresholds, the 400 ms / 75% block geometry and thelfilterrecurrence were audited against the Recommendation and are unchanged.pytest mlx_audio/tests/is green.