@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.
- package/README.md +5 -0
- package/lufs.js +55 -0
- package/package.json +34 -0
package/README.md
ADDED
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
|
+
}
|