first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
# @suaveplan/audio-analysis-loudness - Universal ITU-R BS.1770-4 loudness analyser: K-weighting, gated integrated LUFS, momentary and short-term loudness, EBU R128 loudness range, 4x oversampled true-peak detection, RMS, sample peak, and per-channel DC offset. Streaming and block-level metering for broadcast compliance, mastering, and auto-normalisation pipelines.
Universal ITU-R BS.1770-4 loudness analyser. Drop-in `AnalysisOp` plugin for
the `@suaveplan/audio-*` family.
<!-- begin:auto-docs -->
## Documentation tree
- [loudness-range](./src/algorithms/loudness-range.md) — `loudness-range` computes the EBU Tech 3342 loudness range (LRA, in LU) — a statistical measure of dynamic spread. It t…
- [lufs](./src/algorithms/lufs.md) — `lufs` contains the kernel of BS.1770-4 loudness measurement: the per-block mean-square energy calculation, the BS.1770…
- [true-peak](./src/algorithms/true-peak.md) — `true-peak` implements true-peak (dBTP) detection via 4× polyphase oversampling, per ITU-R BS.1770-4 Annex 2. Sample pe…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable, machine-readable error-code registry for `@suaveplan/audio-analysis-loudness`. Every fault…
- [error-codes](./src/error-codes/error-codes.md) — This module owns the stable, machine-readable error-code registry for `@suaveplan/audio-analysis-loudness`. Every fault…
- [filters](./src/filters/k-weighting.md) — `k-weighting` implements the ITU-R BS.1770 K-weighting filter — a two-stage IIR biquad cascade applied per channel befo…
- [k-weighting](./src/filters/k-weighting.md) — `k-weighting` implements the ITU-R BS.1770 K-weighting filter — a two-stage IIR biquad cascade applied per channel befo…
- [meter](./src/meter/loudness-meter.md) — `LoudnessMeter` is the orchestration layer of `@suaveplan/audio-analysis-loudness`. It wires the K-weighting filter, th…
- [loudness-meter](./src/meter/loudness-meter.md) — `LoudnessMeter` is the orchestration layer of `@suaveplan/audio-analysis-loudness`. It wires the K-weighting filter, th…
<!-- end:auto-docs -->
## Features
- Integrated LUFS via two-gate algorithm (absolute −70, relative −10 LU).
- Momentary (400 ms) and short-term (3 s) loudness.
- EBU Tech 3342 loudness range (LRA in LU).
- 4× oversampled true-peak detection (dBTP, per BS.1770-4 Annex 2).
- Sample peak (dB), RMS (dB), per-channel DC offset.
- Streaming `analyzeStream` whose final yield equals batch `analyze`.
- Real-time `analyzeBlock` for live meters.
- Finite silence floors — no `-Infinity` ever leaks to consumers.
- OTel span `audio.analysis.loudness`, counter, histograms.
- Pure universal: no `window`, no `document`, no `node:fs`.
## Install
```bash
bun add @suaveplan/audio-analysis-loudness
```
## Quick start
```ts
import { LoudnessMeter } from "@suaveplan/audio-analysis-loudness";
const meter = new LoudnessMeter();
const result = await meter.analyze(doc);
console.log({
integrated: result.lufs,
range: result.loudnessRange,
truePeak: result.truePeak,
});
```
## License
MIT

View File

@@ -0,0 +1,66 @@
# loudness-range
## Purpose
`loudness-range` computes the EBU Tech 3342 loudness range (LRA, in LU) — a
statistical measure of dynamic spread. It takes short-term loudness samples
(K-weighted, channel-summed loudness over 3 s windows stepped by 1 s) and
returns the 95th-minus-10th percentile spread of the gated distribution.
LRA complements integrated loudness: two programmes can share an integrated
LUFS but have wildly different listener experience if one is uniformly
compressed and the other has dynamic swings. Broadcasters use LRA thresholds
(e.g. EBU R128 recommends ≤ 7 LU for spoken-word programming) to flag
material that needs dynamics processing before transmission.
## Features
- Two-gate algorithm: absolute gate at −70 LUFS, relative gate 20 LU below
the gated mean (wider than the BS.1770-4 integrated-loudness 10 LU gate).
- Linear-interpolated percentiles per Tech 3342.
- Returns `lra`, `low`, `high`, and `gatedCount` for downstream UIs.
- Sub-window documents safely report `lra: 0` instead of throwing.
## Usage: Basic
```ts
import { loudnessRange } from "@suaveplan/audio-analysis-loudness/algorithms/loudness-range";
const { lra } = loudnessRange(shortTermLufsValues);
```
## Usage: Advanced
```ts
import {
loudnessRange,
percentile,
} from "@suaveplan/audio-analysis-loudness/algorithms/loudness-range";
const result = loudnessRange(shortTermLufsValues);
console.log({
lra: result.lra,
p10: result.low,
p95: result.high,
surviving: result.gatedCount,
});
// Inspect arbitrary percentiles for diagnostic overlays.
const sorted = [...shortTermLufsValues].sort((a, b) => a - b);
const p50 = percentile(sorted, 0.5);
```
## API Reference
- `LRA_RELATIVE_GATE_OFFSET_LU` — `-20`, the Tech 3342 relative-gate offset.
- `loudnessRange(shortTermLufs: readonly number[]) => LoudnessRangeResult`.
- `percentile(sortedAsc, q)` — linear-interpolated q ∈ [0,1].
## Implementation Notes
The mean used for the relative gate is computed in linear power and then
converted back to LUFS via `energyToLufs`, not as an arithmetic mean of LUFS
values — averaging in dB would over-weight quiet blocks. Edge cases (no
input, no survivors after the absolute gate, no survivors after the relative
gate) all map cleanly to `lra: 0` with the best-available `low`/`high` so
downstream consumers never see NaN.

View File

@@ -0,0 +1,57 @@
# lufs
## Purpose
`lufs` contains the kernel of BS.1770-4 loudness measurement: the per-block
mean-square energy calculation, the BS.1770 channel-weighted summation, the
energy-to-LUFS conversion, and the streaming `IntegratedLoudness` accumulator
that implements the two-gate algorithm (absolute gate at −70 LUFS, then a
relative gate 10 LU below the ungated mean of survivors). The kernels are
document-agnostic — they consume `Float32Array[]` blocks and numbers — so a
true-peak limiter can pull `momentaryLufs` without instantiating a meter.
## Features
- Pure functions: `blockMeanSquare`, `combineChannelEnergies`,
`energyToLufs`, `momentaryLufs`, `shortTermLufs`.
- Streaming `IntegratedLoudness` class with two-gate integration.
- BS.1770 channel weights — L/R/C at 1.0, surround at 1.41, LFE excluded.
- Finite silence floor (`LUFS_SILENCE_FLOOR = -100`) instead of `-Infinity`.
## Usage: Basic
```ts
import { momentaryLufs } from "@suaveplan/audio-analysis-loudness/algorithms/lufs";
const lufs = momentaryLufs(kWeightedBlock);
```
## Usage: Advanced
```ts
import { IntegratedLoudness } from "@suaveplan/audio-analysis-loudness/algorithms/lufs";
const integrator = new IntegratedLoudness(2);
for (const block of kWeightedGatingBlocks) integrator.pushBlock(block);
const integrated = integrator.integrated();
console.log({ integrated, surviving: integrator.gatedBlockCount });
```
## API Reference
- `LUFS_SILENCE_FLOOR`, `ABSOLUTE_GATE_LUFS`, `RELATIVE_GATE_OFFSET_LU`,
`CHANNEL_WEIGHTS_BS1770` — frozen constants.
- `channelWeight(index)`, `blockMeanSquare(block)`,
`combineChannelEnergies(perChannel)`, `energyToLufs(weightedMS)`,
`momentaryLufs(block)`, `shortTermLufs(block)`.
- `class IntegratedLoudness` — `pushBlock`, `integrated`, `reset`,
`gatedBlockCount`.
## Implementation Notes
The integrator stores the linear weighted mean-square values that survived
the absolute gate rather than their LUFS counterparts, because the relative
gate is itself derived from the linear mean. This avoids a second
`Math.pow(10, …)` pass per block. The silence floor is the BS.1770-4 minimum
reported value (−70 LUFS for "no programme") rounded down to −100 to give
downstream UI meters a stable bottom.

View File

@@ -0,0 +1,56 @@
# true-peak
## Purpose
`true-peak` implements true-peak (dBTP) detection via 4× polyphase
oversampling, per ITU-R BS.1770-4 Annex 2. Sample peak — `max |x[n]|` — under-
reports the real analog peak because the true waveform crests *between*
samples; near-Nyquist signals routinely show inter-sample peaks 1–3 dB above
the sample peak. Broadcast normalisation pipelines rely on dBTP to keep
material clear of the digital ceiling after DAC reconstruction.
## Features
- 4-phase polyphase FIR with 12-tap kernel per phase.
- `truePeakDbtp(block)` — true-peak across all channels in dBTP.
- `samplePeakLinear(block)` — sample peak in linear amplitude.
- Finite floor at −200 dBTP for digital silence (no `-Infinity`).
- Edge-aware: taps falling outside the buffer are zero-padded.
## Usage: Basic
```ts
import { truePeakDbtp } from "@suaveplan/audio-analysis-loudness/algorithms/true-peak";
const dbtp = truePeakDbtp([leftChannel, rightChannel]);
```
## Usage: Advanced
```ts
import {
truePeakDbtp,
samplePeakLinear,
} from "@suaveplan/audio-analysis-loudness/algorithms/true-peak";
const tp = truePeakDbtp(channels);
const sp = 20 * Math.log10(samplePeakLinear(channels));
console.log({ samplePeakDb: sp, truePeakDbtp: tp, intersampleMargin: tp - sp });
```
## API Reference
- `truePeakDbtp(block: readonly Float32Array[]) => number` — dBTP across all
channels.
- `samplePeakLinear(block: readonly Float32Array[]) => number` — linear-
amplitude sample peak.
## Implementation Notes
The polyphase kernel is a fixed 12-tap windowed-sinc low-pass (Kaiser β=8,
cutoff 0.45·Nyquist), normalised per phase to unity DC gain. Phase 0 is the
identity phase so the original samples are always included in the search and
`truePeakDbtp ≥ samplePeakDb` always holds. BS.1770-4 permits higher
oversampling ratios; the meter sticks to the specified minimum of 4× because
it is sufficient up to 48 kHz material — a configurable ratio would add
surface area for negligible accuracy gain in the target range.

View File

@@ -0,0 +1,77 @@
# error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module owns the stable, machine-readable error-code registry for
`@suaveplan/audio-analysis-loudness`. Every fault raised by the loudness
analyser is reported through a single `AudioAnalysisLoudnessError` subclass of
`SuaveplanError`, carrying a code drawn from `AUDIO_ANALYSIS_LOUDNESS_CODES`
and rich context such as Zod issue lists or abort reasons.
The registry exists so downstream observability, alerting, and i18n layers
can bind to stable string identifiers instead of fragile message strings.
Codes never change once shipped — new fault classes get new codes, never
re-purposed old ones.
## Features
- Three stable codes: `INVALID_OPTIONS`, `EMPTY_DOCUMENT`, `ABORTED`.
- Frozen metadata table (`description`, `severity`) keyed by code.
- `AudioAnalysisLoudnessError` subclass with public constructor and a
`category` discriminant that satisfies the no-suppression-comment rule
without any `// biome-ignore`.
- Idempotent `registerAudioAnalysisLoudnessErrorCodes()` hook.
## Usage: Basic
```ts
import {
AUDIO_ANALYSIS_LOUDNESS_CODES,
AudioAnalysisLoudnessError,
} from "@suaveplan/audio-analysis-loudness/error-codes";
throw new AudioAnalysisLoudnessError(
AUDIO_ANALYSIS_LOUDNESS_CODES.EMPTY_DOCUMENT,
"audio-analysis-loudness.empty-document",
{ context: { sourceLength: 0 } },
);
```
## Usage: Advanced
```ts
import {
AUDIO_ANALYSIS_LOUDNESS_CODES,
AUDIO_ANALYSIS_LOUDNESS_ERROR_METADATA,
registerAudioAnalysisLoudnessErrorCodes,
} from "@suaveplan/audio-analysis-loudness/error-codes";
registerAudioAnalysisLoudnessErrorCodes();
for (const code of Object.values(AUDIO_ANALYSIS_LOUDNESS_CODES)) {
const meta = AUDIO_ANALYSIS_LOUDNESS_ERROR_METADATA[code];
console.log(code, meta.severity, meta.description);
}
```
## API Reference
- `AUDIO_ANALYSIS_LOUDNESS_CODES` — frozen object mapping reason names to
namespaced code strings.
- `AudioAnalysisLoudnessErrorCode` — union of every code string.
- `AUDIO_ANALYSIS_LOUDNESS_ERROR_METADATA` — frozen lookup keyed by code.
- `AudioAnalysisLoudnessError` — `SuaveplanError` subclass.
- `registerAudioAnalysisLoudnessErrorCodes()` — idempotent startup hook.
## Implementation Notes
`defineCodes` produces both the runtime object and the literal type union; the
returned object is frozen, so attempts to mutate codes at runtime fail in
strict mode. The metadata table is independently `Object.freeze`-d so consumers
cannot mutate descriptions or severities. The `category` field on the error
class is what keeps the subclass "non-trivial" enough for the static
analyser — without it, biome flags the override as useless.

View File

@@ -0,0 +1,71 @@
# k-weighting
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`k-weighting` implements the ITU-R BS.1770 K-weighting filter — a two-stage
IIR biquad cascade applied per channel before any loudness energy is
accumulated. The first stage is a high-shelf "pre-filter" approximating the
acoustic effect of the listener's head (≈ +4 dB lift above ~1500 Hz); the
second stage is an RLB high-pass that rolls off content below ~100 Hz so
sub-bass rumble does not dominate the measurement.
Without K-weighting, "loudness" would be raw RMS — which BS.1770 explicitly
rejects because raw RMS over-weights low-frequency energy that the human ear
perceives as quiet. The K-curve is what makes BS.1770 loudness correlate with
perceived programme loudness across genres.
## Features
- Canonical BS.1770 coefficients at the 48 kHz reference rate.
- Rate-agnostic coefficient derivation via inverse bilinear transform —
44.1 kHz, 88.2 kHz, 96 kHz, and arbitrary rates all reproduce the
BS.1770 magnitude response within floating-point tolerance.
- Per-channel direct-form-II transposed state so block-by-block filtering is
sample-identical to a whole-buffer pass.
- Pure `Float32Array[]` in/out — no `AudioBuffer`, no platform globals.
## Usage: Basic
```ts
import { createKWeighting } from "@suaveplan/audio-analysis-loudness/filters/k-weighting";
const filter = createKWeighting(48000, 2);
const filtered = filter.processBlock([leftChannel, rightChannel]);
```
## Usage: Advanced
```ts
import { buildKWeighting, createKWeighting } from "@suaveplan/audio-analysis-loudness/filters/k-weighting";
// Inspect the coefficients for an arbitrary rate.
const { pre, rlb } = buildKWeighting(44100);
// Stream blocks through a stateful filter — output equals a single-pass run.
const filter = createKWeighting(44100, 1);
for (const block of stream) filter.processBlock(block);
filter.reset(); // clear state between unrelated signals
```
## API Reference
- `buildKWeighting(sampleRate)` → `{ pre: Biquad; rlb: Biquad }`. Returns
fresh objects; mutating them does not affect future calls.
- `createKWeighting(sampleRate, channels)` → `KWeightingFilter`.
- `KWeightingFilter.processBlock(block)` → `Float32Array[]` of identical
shape; never mutates the input.
- `KWeightingFilter.reset()` — zeroes all per-channel biquad state.
## Implementation Notes
Coefficients at non-reference rates are derived by recovering the analog
prototype from the 48 kHz biquad (inverse bilinear transform) and then
re-discretising at the target rate (forward bilinear). This avoids the
warping that would occur if the canonical coefficients were used verbatim at
44.1 kHz, where the magnitude response would shift by ~10% in the shelf
region. State arrays are typed `Float64Array` so accumulated rounding stays
below `Float32` quantisation across hour-long buffers.

View File

@@ -0,0 +1,85 @@
# loudness-meter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`LoudnessMeter` is the orchestration layer of `@suaveplan/audio-analysis-loudness`.
It wires the K-weighting filter, the gated-integration engine, the short-term
LUFS accumulator (for loudness range), the 4× oversampled true-peak detector,
and the RMS / sample-peak / DC-offset accumulators together behind three
entry points — full-document `analyze`, streaming `analyzeStream`, and the
real-time `analyzeBlock`. It is a sidecar processor (plugin slot 3) and
never participates in the render pipeline.
The class also threads OTel spans, the run counter, and per-block histograms
through every entry point, validates all options at the Zod boundary, and
propagates `AbortSignal` cancellation to mid-stream block boundaries.
## Features
- ITU-R BS.1770-4 integrated LUFS via two-gate (absolute + relative).
- EBU Tech 3342 loudness range (LRA in LU).
- 4× oversampled true-peak in dBTP (per BS.1770-4 Annex 2).
- Sample peak in dB, RMS in dB, per-channel DC offset.
- Streaming `analyzeStream` whose final yield equals batch `analyze`.
- Real-time `analyzeBlock` for live meters — synchronous, allocation-light.
- Finite silence floors (no `-Infinity` ever leaks to consumers).
- `AbortSignal` propagation at gating-block boundaries.
- OTel span, counter, histograms — inert without an exporter.
## Usage: Basic
```ts
import { LoudnessMeter } from "@suaveplan/audio-analysis-loudness";
const meter = new LoudnessMeter();
const result = await meter.analyze(doc);
console.log(result.lufs, result.truePeak, result.loudnessRange);
```
## Usage: Advanced
```ts
import { LoudnessMeter } from "@suaveplan/audio-analysis-loudness";
const meter = new LoudnessMeter({ blockSize: 400, overlap: 0.75, targetLufs: -23 });
// Streaming integrated loudness — final yield equals batch analyze.
const ctrl = new AbortController();
for await (const partial of meter.analyzeStream(doc, { signal: ctrl.signal })) {
ui.updateLoudness(partial.lufs);
}
// Real-time block meter — call on every audio callback.
const partial = meter.analyzeBlock([left, right], {
sampleRate: 48000,
channelCount: 2,
});
console.log(partial.lufs, partial.peak, partial.rms);
```
## API Reference
- `class LoudnessMeter`
- `readonly name: "loudness-meter"`, `readonly description: string`,
`readonly version: string`.
- `constructor(options?: LoudnessMeterOptions)`.
- `analyze(doc, opts?) => Promise<LoudnessResult>`.
- `analyzeStream(doc, opts?) => AsyncGenerator<LoudnessResult>`.
- `analyzeBlock(block, ctx) => Partial<LoudnessResult>`.
- `LoudnessMeterOptions` — `blockSize`, `overlap`, `targetLufs`, `signal`.
- `LoudnessResult` extends `AnalysisResult` with `lufs`, `loudnessRange`,
`truePeak`, `rms`, `peak`, `dcOffset[]`.
## Implementation Notes
`analyze` and `analyzeStream` share one `GatingEngine`. The streaming path
yields the engine's running state after every gating block; the batch path
runs the same engine to completion. The invariant is that the last
`analyzeStream` yield equals the `analyze` result for the same document.
`analyzeBlock` keeps a per-instance scratch K-weighting filter so live-meter
callers do not allocate on every audio callback. The scratch filter is
re-built lazily when `(sampleRate, channelCount)` changes.