@audio/loudness-lufs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/lufs.js +55 -0
  3. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @audio/loudness-lufs
2
+
3
+ > Integrated / momentary / short-term loudness — ITU-R BS.1770-4 gated blocks (K-weighted)
4
+
5
+ Planned — not implemented yet. See the umbrella README for status and sources.
package/lufs.js ADDED
@@ -0,0 +1,55 @@
1
+ // Integrated loudness (LUFS) per ITU-R BS.1770-4:
2
+ // K-weighting → 400 ms gating blocks at 75% overlap → absolute gate (−70 LUFS) →
3
+ // relative gate (−10 LU below the abs-gated mean) → L = −0.691 + 10·log10(Σ_c G_c·z̄_c).
4
+ // Verified against EBU Tech 3341 minimum-requirements test vectors.
5
+
6
+ import kWeighting from '@audio/weighting-k'
7
+
8
+ const OFFSET = -0.691, ABS_GATE = -70, REL_GATE = -10
9
+ const GATE_WINDOW = 0.4, GATE_HOP = 0.1
10
+
11
+ /**
12
+ * @param {Float32Array|Float32Array[]} channels — mono buffer or array of channel buffers
13
+ * @param {object} opts — { fs, weights } — weights default 1.0 per channel
14
+ * (BS.1770-4 Table 1: pass 1.41 for Ls/Rs surrounds)
15
+ * @returns {number|null} integrated LUFS, or null for silence / all-gated input
16
+ */
17
+ export default function lufs (channels, { fs = 48000, weights } = {}) {
18
+ if (channels[0]?.length === undefined) channels = [channels]
19
+ let G = weights || channels.map(() => 1)
20
+
21
+ // K-weighted copies
22
+ let k = channels.map(ch => {
23
+ let c = Float32Array.from(ch)
24
+ kWeighting(c, { fs })
25
+ return c
26
+ })
27
+
28
+ let win = Math.round(GATE_WINDOW * fs)
29
+ let hop = Math.round(GATE_HOP * fs)
30
+ let n = k[0].length
31
+ if (n < win) return null
32
+
33
+ // per-block power: Σ_c G_c · mean-square over the 400 ms window
34
+ let blocks = []
35
+ for (let i = 0; i + win <= n; i += hop) {
36
+ let sum = 0
37
+ for (let c = 0; c < k.length; c++) {
38
+ let z = 0, ch = k[c]
39
+ for (let j = i; j < i + win; j++) z += ch[j] * ch[j]
40
+ sum += G[c] * z / win
41
+ }
42
+ blocks.push(sum)
43
+ }
44
+
45
+ let absT = 10 ** ((ABS_GATE - OFFSET) / 10)
46
+ let gated = blocks.filter(p => p > absT)
47
+ if (!gated.length) return null
48
+
49
+ let mean = gated.reduce((a, b) => a + b, 0) / gated.length
50
+ let relT = mean * 10 ** (REL_GATE / 10)
51
+ let final = gated.filter(p => p > relT)
52
+ if (!final.length) return null
53
+
54
+ return OFFSET + 10 * Math.log10(final.reduce((a, b) => a + b, 0) / final.length)
55
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@audio/loudness-lufs",
3
+ "version": "1.0.0",
4
+ "description": "Integrated loudness (LUFS) \u2014 ITU-R BS.1770-4 K-weighted gated measurement, EBU Tech 3341-verified",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "lufs.js",
8
+ "exports": {
9
+ ".": "./lufs.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lufs.js"
14
+ ],
15
+ "dependencies": {
16
+ "@audio/weighting-k": "^1.0.0"
17
+ },
18
+ "keywords": [
19
+ "audio",
20
+ "dsp",
21
+ "loudness",
22
+ "lufs",
23
+ "bs1770",
24
+ "ebu-r128"
25
+ ],
26
+ "license": "MIT",
27
+ "author": "Dmitry Iv. <dfcreative@gmail.com>",
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }