feat: programme loudness and true peak (ITU-R BS.1770-5 / EBU R 128) (#219)
* refactor: extract the shared inter-sample-peak oversampling helper
The polyphase oversample-then-max machinery of lc_peak also underpins the
BS.1770-5 true-peak meter, so it moves to _internal/peaks.py as
inter_sample_peak and lc_peak delegates to it (bit-identical behaviour).
* feat: programme loudness and true peak (ITU-R BS.1770-5 / EBU R 128)
New broadcast domain implementing the objective multichannel loudness
measurement algorithm and its EBU companions:
- k_weighting / k_weighting_coefficients: the Annex 1 two-stage pre-filter
with the Table 1-2 biquads verbatim at 48 kHz and an analog-prototype
redesign at other rates (response preserved within 0.016 dB).
- integrated_loudness: 400 ms gating blocks at 75 % overlap, Table 3
channel weights (LFE excluded) and the two-stage -70 LKFS / -10 LU gate
(Formulae 1-7).
- program_loudness: the full EBU Mode set of Tech 3341 as a frozen
ProgramLoudnessResult with .plot() (momentary and short-term series with
maxima, integrated loudness, relative threshold, loudness range with its
percentile edges, per-channel true peaks).
- loudness_range: the Tech 3342 algorithm with its cascaded -70 LUFS /
-20 LU gate and 10th-95th percentile spread, following the reference
implementation's nearest-rank indexing.
- true_peak_level: the Annex 2 inter-sample peak in dBTP by oversampling
to at least 192 kHz (4x at 48 kHz), sharing the lc_peak machinery.
- channel_weight: the Annex 3 position-dependent weights (Table 4) for
the BS.2051 advanced sound systems.
* test: EBU Tech 3341/3342 minimum-requirements signals for programme loudness
Every synthesizable case runs at its official tolerance: Tech 3341 cases
1-6 (integrated, +/-0.1 LU), 9-14 (momentary/short-term dynamics and
maxima, including the 20-step live sequences), 15-19 (true-peak tones,
+0.2/-0.4 dB) and 20-23 (the fs/4 period inside an fs/6 tone decimated
with 0-3 sample offsets); Tech 3342 LRA cases 1-4 (+/-1 LU) plus the
repetition-invariance clause. Alongside: the 997 Hz / -3.01 LKFS anchor
on L/C/R, the verbatim 48 kHz coefficient tables and the redesign match
at 44.1/96/192 kHz, the Annex 2 Attachment 1 under-read bound, the
Annex 3 Table 4 weight grid, LFE exclusion, gating behaviour and the
validation/edge paths. The case tables live in tests/reference_data.py,
shared with the conformance report.
* feat: programme-loudness conformance checks (8 EBU cases)
New 'Program loudness (ITU-R BS.1770 / EBU R 128)' domain in the
conformance registry: the Annex 1 997 Hz anchor, Tech 3341 integrated
cases 1/5/6, true-peak cases 15/19 and Tech 3342 LRA cases 1/3, all
synthesized from the shared reference tables. 274 checks total.
* docs: programme-loudness metering figure (EN/ES, light/dark)
A one-minute synthetic programme (ambience, dialogue, music, fade-out)
loudness-normalised to the R 128 target: momentary and short-term traces,
the integrated loudness on -23.0 LUFS, the LRA band between its
percentile edges and the summary box with the maxima and true peak.
* docs: programme-loudness guide (EN/ES), indices and API reference
The guide walks the chain from the K-weighting and the 997 Hz anchor
through gating, the EBU Mode time scales, the loudness range, the
true-peak meter and the Annex 3 multichannel weights, with the official
EBU signals as inline examples and the validation scope spelled out.
Wired into the documentation index, the sources-and-devices section
landing (EN/ES), the sidebar, the curated API quick table (thirteen
domains now), the generated API reference taxonomy, the bibliography and
the changelog.
* docs: add the programme-loudness guide to the README feature table
* fix: address programme-loudness review findings
- Reject sample rates below 16 kHz in the K-weighting redesign: at 8 kHz
the bilinear warping reaches 0.55 dB in band and drifts the 997 Hz
anchor to -2.89 LKFS, outside the +/-0.1 LU metering tolerance. The
response claim is now scoped honestly (within 0.02 dB at 32 kHz and
above) in the docstrings, the guides and the changelog.
- Reject non-finite input in integrated_loudness/program_loudness: a
single NaN would poison every gating block and silently read as
digital silence (-inf).
- Correct the EBU R 128 tolerance prose in the three guides: the 2023
edition permits +/-1.0 LU where the target is not practically
achievable and +/-0.2 LU for measurement error in QC workflows (the
+/-0.5 LU general tolerance was the 2014 rule).
- Drop the false 'Annex 2 minimum of 4 below 96 kHz' clause from the
true_peak_level docstring (the default is the smallest factor whose
oversampled rate reaches 192 kHz) and regenerate the API page.
- Reject a boolean oversample factor, make the second K-weighting
snippet self-contained, drop an unused unpacked variable in the
conformance checks and a redundant loudness-range guard.
* test: run the authentic-programme EBU loudness cases against the official test set
Tech 3341 cases 7-8 and Tech 3342 cases 5-6 use authentic programme
material whose licence covers technical testing only, so the audio is never
committed: the tests read the official EBU loudness test set from a
gitignored local directory (EBU_LOUDNESS_TEST_SET overrides it) and skip
when it is absent. One CI matrix cell fetches the set from the EBU with an
Actions cache so the four cases also run on every pull request. All four
pass within the official tolerances.
* fix: harden the EBU test-set download and address the loudness review smells
The EBU site rejects curl's default agent, so the CI download sends a
browser agent, verifies a sentinel file and drops the directory on failure
so a bad download is never cached (fresh cache key). The duplicated
empty-signal message becomes a module constant and the exception tests
build their inputs outside the raises blocks.
* test: pin the gating and LRA stages to committed authentic-programme series
The audio of the EBU authentic-programme cases cannot be committed, but the
per-block loudness series measured from it are plain measurement data that
cannot reconstruct the programmes. The 400 ms gate-input and 3 s short-term
series of both files are committed (26 kB, rounded three orders of magnitude
below the official tolerances) and the integrated-gating and loudness-range
stages now verify the four case values on every platform, network or not.
The full chain still runs against the original audio wherever the set is
present.
* ci: send a full browser agent when fetching the EBU loudness test set
* fix: apply the programme-loudness review round
The Tech 3341 true-peak conformance checks now judge with the asymmetric
+0.2/-0.4 dB window the specification defines. Long programmes recover the
inter-sample peak in bounded chunks (with a margin far beyond the FIR
support, verified exactly equal to the whole-signal computation) so the
memory use no longer scales with duration. Non-finite loudspeaker positions
and channel weights are rejected, the momentary and short-term steps
enforce the 10 Hz minimum update rate the API documents, the curated API
table states the real 16 kHz floor, the plot's short-term label became a
caller-overridable default, the download sentinel and the oracle skip check
both files, the series test closes its npz handle and the Spanish landing
names the exact BS.1770-5 revision.
feat: programme loudness and true peak (ITU-R BS.1770-5 / EBU R 128) (#219)
* refactor: extract the shared inter-sample-peak oversampling helper
The polyphase oversample-then-max machinery of lc_peak also underpins the
BS.1770-5 true-peak meter, so it moves to _internal/peaks.py as
inter_sample_peak and lc_peak delegates to it (bit-identical behaviour).
* feat: programme loudness and true peak (ITU-R BS.1770-5 / EBU R 128)
New broadcast domain implementing the objective multichannel loudness
measurement algorithm and its EBU companions:
- k_weighting / k_weighting_coefficients: the Annex 1 two-stage pre-filter
with the Table 1-2 biquads verbatim at 48 kHz and an analog-prototype
redesign at other rates (response preserved within 0.016 dB).
- integrated_loudness: 400 ms gating blocks at 75 % overlap, Table 3
channel weights (LFE excluded) and the two-stage -70 LKFS / -10 LU gate
(Formulae 1-7).
- program_loudness: the full EBU Mode set of Tech 3341 as a frozen
ProgramLoudnessResult with .plot() (momentary and short-term series with
maxima, integrated loudness, relative threshold, loudness range with its
percentile edges, per-channel true peaks).
- loudness_range: the Tech 3342 algorithm with its cascaded -70 LUFS /
-20 LU gate and 10th-95th percentile spread, following the reference
implementation's nearest-rank indexing.
- true_peak_level: the Annex 2 inter-sample peak in dBTP by oversampling
to at least 192 kHz (4x at 48 kHz), sharing the lc_peak machinery.
- channel_weight: the Annex 3 position-dependent weights (Table 4) for
the BS.2051 advanced sound systems.
* test: EBU Tech 3341/3342 minimum-requirements signals for programme loudness
Every synthesizable case runs at its official tolerance: Tech 3341 cases
1-6 (integrated, +/-0.1 LU), 9-14 (momentary/short-term dynamics and
maxima, including the 20-step live sequences), 15-19 (true-peak tones,
+0.2/-0.4 dB) and 20-23 (the fs/4 period inside an fs/6 tone decimated
with 0-3 sample offsets); Tech 3342 LRA cases 1-4 (+/-1 LU) plus the
repetition-invariance clause. Alongside: the 997 Hz / -3.01 LKFS anchor
on L/C/R, the verbatim 48 kHz coefficient tables and the redesign match
at 44.1/96/192 kHz, the Annex 2 Attachment 1 under-read bound, the
Annex 3 Table 4 weight grid, LFE exclusion, gating behaviour and the
validation/edge paths. The case tables live in tests/reference_data.py,
shared with the conformance report.
* feat: programme-loudness conformance checks (8 EBU cases)
New 'Program loudness (ITU-R BS.1770 / EBU R 128)' domain in the
conformance registry: the Annex 1 997 Hz anchor, Tech 3341 integrated
cases 1/5/6, true-peak cases 15/19 and Tech 3342 LRA cases 1/3, all
synthesized from the shared reference tables. 274 checks total.
* docs: programme-loudness metering figure (EN/ES, light/dark)
A one-minute synthetic programme (ambience, dialogue, music, fade-out)
loudness-normalised to the R 128 target: momentary and short-term traces,
the integrated loudness on -23.0 LUFS, the LRA band between its
percentile edges and the summary box with the maxima and true peak.
* docs: programme-loudness guide (EN/ES), indices and API reference
The guide walks the chain from the K-weighting and the 997 Hz anchor
through gating, the EBU Mode time scales, the loudness range, the
true-peak meter and the Annex 3 multichannel weights, with the official
EBU signals as inline examples and the validation scope spelled out.
Wired into the documentation index, the sources-and-devices section
landing (EN/ES), the sidebar, the curated API quick table (thirteen
domains now), the generated API reference taxonomy, the bibliography and
the changelog.
* docs: add the programme-loudness guide to the README feature table
* fix: address programme-loudness review findings
- Reject sample rates below 16 kHz in the K-weighting redesign: at 8 kHz
the bilinear warping reaches 0.55 dB in band and drifts the 997 Hz
anchor to -2.89 LKFS, outside the +/-0.1 LU metering tolerance. The
response claim is now scoped honestly (within 0.02 dB at 32 kHz and
above) in the docstrings, the guides and the changelog.
- Reject non-finite input in integrated_loudness/program_loudness: a
single NaN would poison every gating block and silently read as
digital silence (-inf).
- Correct the EBU R 128 tolerance prose in the three guides: the 2023
edition permits +/-1.0 LU where the target is not practically
achievable and +/-0.2 LU for measurement error in QC workflows (the
+/-0.5 LU general tolerance was the 2014 rule).
- Drop the false 'Annex 2 minimum of 4 below 96 kHz' clause from the
true_peak_level docstring (the default is the smallest factor whose
oversampled rate reaches 192 kHz) and regenerate the API page.
- Reject a boolean oversample factor, make the second K-weighting
snippet self-contained, drop an unused unpacked variable in the
conformance checks and a redundant loudness-range guard.
* test: run the authentic-programme EBU loudness cases against the official test set
Tech 3341 cases 7-8 and Tech 3342 cases 5-6 use authentic programme
material whose licence covers technical testing only, so the audio is never
committed: the tests read the official EBU loudness test set from a
gitignored local directory (EBU_LOUDNESS_TEST_SET overrides it) and skip
when it is absent. One CI matrix cell fetches the set from the EBU with an
Actions cache so the four cases also run on every pull request. All four
pass within the official tolerances.
* fix: harden the EBU test-set download and address the loudness review smells
The EBU site rejects curl's default agent, so the CI download sends a
browser agent, verifies a sentinel file and drops the directory on failure
so a bad download is never cached (fresh cache key). The duplicated
empty-signal message becomes a module constant and the exception tests
build their inputs outside the raises blocks.
* test: pin the gating and LRA stages to committed authentic-programme series
The audio of the EBU authentic-programme cases cannot be committed, but the
per-block loudness series measured from it are plain measurement data that
cannot reconstruct the programmes. The 400 ms gate-input and 3 s short-term
series of both files are committed (26 kB, rounded three orders of magnitude
below the official tolerances) and the integrated-gating and loudness-range
stages now verify the four case values on every platform, network or not.
The full chain still runs against the original audio wherever the set is
present.
* ci: send a full browser agent when fetching the EBU loudness test set
* fix: apply the programme-loudness review round
The Tech 3341 true-peak conformance checks now judge with the asymmetric
+0.2/-0.4 dB window the specification defines. Long programmes recover the
inter-sample peak in bounded chunks (with a margin far beyond the FIR
support, verified exactly equal to the whole-signal computation) so the
memory use no longer scales with duration. Non-finite loudspeaker positions
and channel weights are rejected, the momentary and short-term steps
enforce the 10 Hz minimum update rate the API documents, the curated API
table states the real 16 kHz floor, the plot's short-term label became a
caller-overridable default, the download sentinel and the oracle skip check
both files, the series test closes its npz handle and the Spanish landing
names the exact BS.1770-5 revision.
Correct fiche verdict rules and mandatory report items across the .report() layer (#281)
* fix(report): apply the current R 128 tolerance rules and the standards' mandatory report items across the fiches
The EBU R 128 programme-loudness fiche passed its verdict on a blanket
+/-0.5 LU about the target, the June 2014 (V3) rule that R 128 dropped in
its August 2020 revision. ProgramLoudnessResult.report() now selects the
tolerance explicitly: tolerance="qc" (default) applies the +/-0.2 LU
measurement-error allowance of item i) for loudness workflows such as
Quality Control, tolerance="live" applies the +/-1.0 LU tolerance of
item h) for programmes where the Target Level is not achievable
practically; the fiche prints the applied rule and its R 128 item, and the
verdict is evaluated on the loudness rounded to the displayed 0.1 LU
(Tech 3341 display precision), so the printed numbers can never contradict
it at the boundary. The committed example signal is trimmed 0.4 dB onto
the -23.0 LUFS target so it passes the QC rule it demonstrates.
Also fixed across the fiche layer:
- language="es" now reaches the embedded chart: the fiche language is
forwarded to the result's own plot(), so Spanish fiches embed Spanish
axis labels and legends instead of English ones.
- ISO 532-1: the fiche states the clause 7 mandatory items: the method
used (stationary, clause 5, or time-varying, clause 6), the sound field
(ZwickerLoudness gains a field attribute set by the constructors), the
loudness-versus-time function N(t) for time-varying sounds and the
"Maximum loudness Nmax" labelling of the reported maximum.
- ICAO Annex 16: effective_perceived_noise_level() gains
procedure="aeroplane"|"helicopter", plumbing the App. 2 4.3.1 Step 1
tone-correction start band (50 Hz for helicopters and tilt-rotors)
through the public chain; the verdict determines the EPNL once to one
decimal place, as the Annex states it, before the level, margin and
status are derived; the reference-conditions strip cites Part II
3.6.1.5 in full (25 C i.e. ISA + 10 C, 70 % RH, 1013.25 hPa, zero wind).
- ISO 717: the caption declares the band set actually rated (an
octave-band rating was captioned one-third-octave, against the clause
4.4 statement requirement); report() gains symbol= so field quantities
(DnT,w, L'nT,w, ...) are not mislabelled with the laboratory Rw / Ln,w
descriptor; adaptation terms print a sign only when negative, the style
of the standard's own examples.
- IEC 61260-1: verify_filter_class() reports range_limited (and per-band
checked_to_omega) when the stop-band mask extends beyond a band's
processing Nyquist, and the fiche qualifies its COMPLIES statement
accordingly; bands are labelled with nominal mid-band frequencies; the
1995 fiche cites that edition's relative-attenuation definition (3.13
Note); a required_class absent from the verified edition is rejected.
- ISO 11654: the shape indicator is written in the clause 5.3 style
(0.60(M), no space) and the 5.3 NOTE recommendation is printed when an
indicator applies.
- Header grids reprint client-supplied metadata verbatim (1.23 m2 no
longer becomes 1.2), values that round to zero never print -0.0, and
the remaining English fragments on the Spanish IEC 61260 fiche are
localised.
Every fiche gains at least one pypdf content test pinning the exact
displayed values, plus boundary tests for the display-rounded verdicts
and the helicopter start band.
* chore: regenerate example fiches, API reference pages and llms files
The example fiches pick up the corrected verdict rules and report items
(the programme-loudness example now demonstrates the default +/-0.2 LU QC
rule on target), and the generated API pages and llms files pick up the
new report keywords and result attributes.
* fix(report): integrate the fiche corrections with the field-report engine
The rebase onto the ISO 16283 fiches left two seams: the verdict strings were
added by both branches (duplicate i18n keys) and the new field renderer's
embedded-plot closure did not accept the language argument the layout now
forwards. Dedupe the table, accept the keyword, and re-verify the rendered
examples.
* fix(report): draw the LRA band as a pale opaque tint so the curves stay visible
The report pipeline renders through svglib, which drops alpha, so the
translucent LRA span came out as saturated orange and hid the momentary
trace. Use the 15 percent composite of the accent over white as an opaque
fill below the curves, matching the other fiches' band treatment.
* fix(report): keep fiche charts full width and legends tidy
Cap the relocated legend at three columns: a single four-entry row (the
impact fiches add the 500 Hz read marker) grew wider than the axes and
tight_layout shrank the chart, leaving the impact fiche visibly narrower
than its airborne sibling. Also compact the field-fiche y-axis labels to
the quantity symbol (the full name already heads the page and the table),
regenerate the affected examples, and drop the now-unused secondary color
import.
* review: remove float equality, reduce complexity and hoist raise-block setup
Sonar flagged the signed-zero normalisation in format_number and the
display rounding's zero check as float equality: normalise the sign by
inspecting the formatted text and compare the non-negative magnitude with
an order relation instead. Extract the per-band evaluation of
verify_filter_class and the loudness fiche's basis line into helpers to
bring both functions under the complexity threshold. Hoist the setup calls
out of four pytest.raises blocks, print the live tolerance through
format_number so it keeps one decimal, and round the loudness metrics row
with the same display rule as the boxed statement.